Vue


上次更新时间:4/29/2022, 9:34:08 AM 0

官网文档介绍《Vue.js》

# MVVM

MVVM 和 MVC 是两种不同的软件设计模式

Vue 和 React 使用的是 MVVM 的设计模式,与传统的 MVC 不同,它通过数据驱动视图。MVVM 模式是组件化的基础。

# MVVM: Model-View-ViewModel,数据驱动视图

  • 各部分之间的通信,都是双向的
  • View 与 Model 不发生联系,通过 viewModel 传递

# MVC: Model-View-Controller

  • View 传送指令到 Controller
  • Controller 完成业务逻辑后,要求 Model 改变状态
  • Model 将新的数据发送到 View,用户得到反馈

在 MVC 下,所有通信都是单向的

# 响应式原理

在不同的vue版本,实现响应式的方法不同:

# Object.defineProperty

Vue 会遍历 data 所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter,每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

# 实现一个简单的响应式

function defineReactive(target, key, value) {
    // 深度监听(对象)
    Observer(value)
    // 核心API - 响应
    Object.defineProperty(target, key, {
        get: function() {
            return value
        },
        set: function(newVal) {
            if (value !== newVal) {
                // 深度监听(对象)
                Observer(newVal)

                value = newVal
                updateView()
            }
        }
    })
}

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

// 重新定义数组原型
const oldArrayProperty = Array.prototype;
// 创建新对象,原型指向 oldArrayProperty,再拓展新方法不会影响新原型
const arrProto = Object.create(oldArrayProperty)
const methods = ['push', 'pop', 'shift', 'unshift', 'splice']
methods.forEach( methodName => {
    arrProto[methodName] = function() {
        updateView(); // 视图更新
        oldArrayProperty[methodName].call(this, ...arguments) // 调用数组原型方法进行更新
    }
});

function Observer(target) {
    if (typeof target !== 'object' || target === null) {
        return target
    }
    // 深度监听(数组)
    if (Array.isArray(target)) {
        target.__proto__ = arrProto
    }

    for (key in target) {
        defineReactive(target, key, target[key])
    }
}

const data = {
    name: 'jack',
    age: 18,
    info: {
        address: '北京'
    },
    nums: [1, 2, 3]
}

// data 实现了双向绑定,深度监听
Observer(data)
 
// data.info.address = '上海' // 深度监听
// data.nums.push(4) // 监听数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

# Object.defineProperty的优势

  • 兼容性好,支持 IE9

# Object.defineProperty不足

  • 无法监听数组的变化
  • 必须遍历对象的每个属性
  • 必须深层遍历嵌套的对象
  • 无法监听新增属性、删除属性
  • 需要在开始时一次性递归所有属性

# Proxy

Proxy 是 es6 新增的内置对象,它用于定义基本操作的自定义行为。可用于运算符重载、对象模拟,对象变化事件、双向绑定等。

# Proxy实现响应式

function reactive(target = {}) {
    if (typeof target !== 'object' || target === null) {
        // 非对象或数组,返回
        return target
    }

    // 代理配置
    const proxyConf = {
        get(target, key, receiver) {
            // 指处理本身(非原型的)属性
            const ownKeys = Reflect.ownKeys(target)
            if (ownKeys.includes(key)) {
                // 监听
            }
            const result = Reflect.get(target, key, receiver)
             // 在进行get的时候,再递归深度监听 - 性能提升
            return reactive(result)
        },
        set(target, key, value, receiver) {
            // 重复数据, 不处理
            if (value === target[key]) {
                return true
            }
            // 指处理本身(非原型的)属性
            const ownKeys = Reflect.ownKeys(target)
            if (ownKeys.includes(key)) {
                console.log('已有的key', key)
            } else {
                console.log('新增的key', key)
            }

            const result = Reflect.set(target, key, value, receiver)
            return result
        },
        deleteProperty(target, key) {
            const result = Reflect.deleteProperty(target, key)
            return result
        }
    }
    
    // 生成代理对象
    const observed = new Proxy(target, proxyConf)
    return observed
}

const data = {
    name: 'jack',
    age: 18,
    info : {
        city: 'beijing'
    }
}

const proxyData = reactive(data)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

# Proxy 的优势:

  • Proxy 可以直接监听对象而非属性,可以监听新增/删除属性;
  • Proxy 可以直接监听数组的变化;
  • Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;
  • Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;

# Proxy 不足:

  • 兼容性问题,而且无法使用 polyfill 抹平(es5 中没有可以模拟Proxy的函数/方法)

# 虚拟Dom

虚拟Dom 也就是 visual dom,常叫为 vdom。vdom 是实现 vue 和 react 的重要基石。

# 浏览器渲染

在了解 vdom 之前,了解一下浏览器的工作原理是很重要的。浏览器在渲染网页时,会有几个步骤,其中一个就是解析HTML,生成 DOM 树。以下面 HTML 为例:

<div>
  <h1>My title</h1>
  Some text content
  <!-- TODO: Add tagline -->
</div>
1
2
3
4
5

当浏览器读到这些代码时,会解析为对应的 DOM 节点树

每一个元素、文字、注释都是一个节点,众所周知,如果直接操作 dom 去更新,是非常耗费性能的,因为每一次的操作都会触发浏览器的重新渲染。Js 的执行相对来说是非常快的,于是,便出现了 vdom。

# snabbdom

snabbdom是一个简洁强大的 vdom 库,易学易用。vue 是参考它实现的 vdom 和 diff 算法。可以通过 snabbdom 学习 vdom。

# vdom

Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM,核心方法是createElement 函数。createElement 函数会生成一个虚拟节点,也就是 vNode,它会告诉浏览器应该渲染什么节点。vdom 是对由 Vue 组件树建立起来的整个 vnode 树的称呼。

使用render方式创建组件能更直观看到 createElement 如何创建一个vnode(render函数的约束

  • createElement(标签名, 属性对象, 文本/子节点数组)
Vue.component('my-component', {
    props: {
        title: {
            type: String,
            default: '标题'
        }
    },
    data() {
        return {
            docUrl: 'https://cn.vuejs.org/v2/guide/render-function.html#%E5%9F%BA%E7%A1%80'
        }
    },
    render(createElement) {
        return createElement(
            'div', 
            {
                'class': 'page-container'
            }, 
            [
                createElement(
                    'h1', 
                    {
                        attrs: {
                            id: 'title'
                        }
                    },
                    this.title
                ),
                createElement(
                    'a', 
                    {
                        attrs: {
                            href: this.docUrl
                        }
                    },
                    'vue文档'
                )
            ]
        )
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

上面方法,会生成一个 vnode 树(即AST 树)

将关键属性抽离出来后,可以看到一个类似于浏览器解析 Html 的节点树。这个结构会被渲染成真正的 Dom,并显示在浏览器上。

{
    "tag": "div",
    "data": {
        "class": "page-container"
    },
    "children": [
        {
            "tag": "h1",
            "data": {
                "attrs": {
                    "id": "title"
                }
            }
        },
        {
            "tag": "a",
            "data": {
                "attrs": {
                    "href": "https://cn.vuejs.org/v2/guide/render-function.html#%E5%9F%BA%E7%A1%80"
                }
            }
        }
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

初次渲染的时候,这个 AST 树会被存储起来,当监听到数据有改变时,将被用来跟新的 vdom 做对比。这个对比的过程使用的是diff算法。

# diff算法

diff 算法是 vdom 中最核心、最关键的部分。vue 的 diff 算法处理位于 patch.js 文件中。

diff 即对比,是一个广泛的概念,不是 vue、react 特有的。如 linux diff 命令,git diff 等。

# 二叉树diff算法

原树 diff 算法需要经历每个节点遍历对比,最后排序的过程。如果有1000个节点,需要计算1000^3=10亿次,时间复杂度为O(n^3)。

很明显,直接使用原 diff 算法是不可行的。

# vue中的diff算法

vue 将 diff 的时间复杂度降低为O(n),主要做了以下的优化:

  • 只比较同一层级,不跨级比较
  • tag 不相同,则直接删掉重建,不再深度比较
  • tag 和 key 两者都相同,则认为是相同节点,不再深度比较

# 模板编译

模板编译是指对 vue 文件内容的编译转换。Vue 的模板实际上被编译成了 render 函数,执行 render 函数返回 vnode。

# with语句

在了解模板编译之前,需要先了解下with 语句。

with语句可以扩展一个语句的作用域链。将某个对象添加到作用域链的顶部,默认查找该对象的属性。

var obj = {a: 100};
// {} 内的自由变量,当做 obj 的属性来查找
with(obj) {
    console.log(a); // 100
    console.log(b); // ReferenceError: b is not defined
}
1
2
3
4
5
6

不被推荐使用,在 ECMAScript 5 严格模式中该标签已被禁止。

# 编译模板

当使用 template 模板的时候,vue 会将模板解析为 AST树(abstract syntax tree,抽象语法树),语法树再通过 generate 函数把 AST树 转化为 render 函数,最后生成 vnode 对象。

核心插件:vue-template-compiler

vue-template-compiler api:

  • compile(): 编译 template 标签内容,并返回一个对象
  • parseComponent(): 将单文件组件或*.vue文件解析成flow declarations
  • compileToFunctions(): 类似 compiler.compile,但直接返回实例化函数
  • ssrCompile(): 类似 compiler.compile ,将部分模板优化成字符串连接来生成特定于SSR的呈现函数代码
  • ssrCompileToFunctions(): 类似 compileToFunction , 将部分模板优化成字符串连接来生成特定于SSR的呈现函数代码
  • generateCodeFrame(): 将 template 标签内容高亮显示

# 举个栗子

template.js

const compiler = require('vue-template-compiler');
const template = '<p>{{message}}</p>'

console.log(compiler.compile(template))
1
2
3
4

执行

# 编译
node template.js
1
2

输出,返回一个这样的对象

{ 
    ast: { 
        type: 1,
        tag: 'p',
        attrsList: [],
        attrsMap: {},
        rawAttrsMap: {},
        parent: undefined,
        children: [ [Object] ],
        plain: true,
        static: false,
        staticRoot: false 
    },
    render: 'with(this){return _c(\'p\',[_v(_s(message))])}',
    staticRenderFns: [],
    errors: [],
    tips: [] 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

使用 webpack 打包,在开发环境 vue-loader 实现了编译

render 中 _c 代表 createElement,其他的缩写函数说明:

function installRenderHelpers (target) {
  target._o = markOnce;
  target._n = toNumber;
  target._s = toString;
  target._l = renderList;
  target._t = renderSlot;
  target._q = looseEqual;
  target._i = looseIndexOf;
  target._m = renderStatic;
  target._f = resolveFilter;
  target._k = checkKeyCodes;
  target._b = bindObjectProps;
  target._v = createTextVNode;
  target._e = createEmptyVNode;
  target._u = resolveScopedSlots;
  target._g = bindObjectListeners;
  target._d = bindDynamicKeys;
  target._p = prependModifier;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

vue-template-compiler 会针对模板中的各种标签、指令、事件进行提取拆分,分别处理。

# 组件渲染与更新

初次渲染

  1. 解析模板为 render 函数(或在开发环境已完成,vue-loader)
  2. 触发响应式,监听 data 属性 getter setter
  3. 执行 render 函数,生成 vnode
  4. path(elem, vnode)

更新过程

  1. 修改 data,触发 setter(此前在 getter 中已被监听)
  2. 重新执行 render 函数,生成 newVnode
  3. path(vnode, newVnode)

# 异步更新

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。

简单来说,事件循环会先执行完所有的宏任务(macro-task),再执行微任务(micro-task)。vue 将所有的更新都插入一个队列,当这个队列执行清空后再调用微任务。而 MutationObserver 、promise.then等都属于微任务(setTimeout属于宏任务)。

nextTick() 是更新后的回调函数,在 nextTick() 可以拿到最新 dom 元素。

验证

<template>
  <div class="hello">
    <ul ref="list">
        <li v-for="(item, index) in list" :key="index">
          {{item}}
        </li>
    </ul>
    
    <button @click="handleClick">点击</button>
  </div>
</template>

<script>
export default {
    data() {
        return {
        list: []
        }
    },
    watch: {
        list: {
        handler: function(val) {
            console.log('watch', val.length) // 3 - 仅触发一次
        },
        deep: true
        }
    },
    methods: {
        handleClick() {
            // 修改 3 次
            this.list.push(1)
            this.list.push(2)
            this.list.push(3)
            console.log('before>>', this.$refs.list.children.length) // 0 - 未更新
            this.$nextTick(() => {
                console.log('after>>', this.$refs.list.children.length) // 3 - 已更新
            })
        }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 源码

定义:nextTick (文件路径:vue/src/core/util/next-tick.js)

var callbacks = []; // 所有需要执行的回调函数
var pending = false; // 状态,是否有正在执行的回调函数

function flushCallbacks () { // 执行callbacks所有的回调
    pending = false;
    var copies = callbacks.slice(0);
    callbacks.length = 0;
    for (var i = 0; i < copies.length; i++) {
        copies[i]();
    }
}

var timerFunc; // 保存正在被执行的函数

/**
 * 延迟调用函数支持的判断
 * 1. Promise.then
 * 2. then、MutationObserver
 * 3. setImmediate
 * 4. setTimeout(fn, 0)
 * */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve();
    timerFunc = function () {
        p.then(flushCallbacks);
        if (isIOS) { setTimeout(noop); }
    };
} else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || MutationObserver.toString() === '[objectMutationObserverConstructor]')) {
    var counter = 1;
    var observer = new MutationObserver(flushCallbacks);
    var textNode = document.createTextNode(String(counter));
    observer.observe(textNode, {
        characterData: true
    });
    timerFunc = function () {
        counter = (counter + 1) % 2;
        textNode.data = String(counter);
    };
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    timerFunc = function () {
        setImmediate(flushCallbacks);
    };
} else {
    timerFunc = function () {
        setTimeout(flushCallbacks, 0);
    };
}

function nextTick (cb, ctx) {
    var _resolve;
    callbacks.push(function () {
        if (cb) {
        try {
            cb.call(ctx);
        } catch (e) {
            handleError(e, ctx, 'nextTick');
        }
        } else if (_resolve) {
            _resolve(ctx);
        }
    });
    if (!pending) {
        pending = true;
        timerFunc();
    }
    // $flow-disable-line
    if (!cb && typeof Promise !== 'undefined') {
        return new Promise(function (resolve) {
        _resolve = resolve;
        })
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

监听变化:update (文件路径:vue/src/core/observer/watcher.js)

// update 默认是异步的
update () {
    /* istanbul ignore else */
    if (this.lazy) {
        this.dirty = true
    } else if (this.sync) {
        /*同步则执行run直接渲染视图*/
        this.run()
    } else {
        /*异步推送到观察者队列中,下一个tick时调用。*/
        queueWatcher(this)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

队列监听:queueWatcher (文件路径:vue/src/core/observer/scheduler.js)

let waiting = false // 是否刷新
let flushing = false // 队列更新状态

// 重置
function resetSchedulerState () {
    index = queue.length = activatedChildren.length = 0
    has = {}
    if (process.env.NODE_ENV !== 'production') {
        circular = {}
    }
    waiting = flushing = false
}

export function queueWatcher (watcher: Watcher) {
    const id = watcher.id
    if (has[id] == null) {
        has[id] = true
        if (!flushing) {
            // 未更新,则加入
            queue.push(watcher)
        } else {
            // 已更新过,把这个watcher再放到当前执行的下一位, 当前的watcher处理完成后, 立即会处理这个最新的
            let i = queue.length - 1
            while (i > index && queue[i].id > watcher.id) {
                i--
            }
            queue.splice(i + 1, 0, watcher)
        }
        // waiting 为false, 等待下一个tick时, 会执行刷新队列
        if (!waiting) {
            waiting = true

            if (process.env.NODE_ENV !== 'production' && !config.async) {
                flushSchedulerQueue()
                return
            }
            // 执行视图更新
            nextTick(flushSchedulerQueue)
        }
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
上次更新时间: 4/29/2022, 9:34:08 AM