关于前端:vue核心原理全解简单易懂

73次阅读

共计 9423 个字符,预计需要花费 24 分钟才能阅读完成。

官网文档介绍《Vue.js》

MVVM

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

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

MVVM

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

  • 各局部之间的通信,都是双向的
  • View 与 Model 不产生分割,通过 viewModel 传递

MVC

MVC: Model-View-Controller

  • View 传送指令到 Controller
  • Controller 实现业务逻辑后,要求 Model 扭转状态
  • Model 将新的数据发送到 View,用户失去反馈

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

响应式原理

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

  • vue2.0:Object.defineProperty
  • vue3.0:Proxy

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) // 监听数组

劣势

  • 兼容性好,反对 IE9

有余

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

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)

劣势

  • Proxy 能够间接监听对象而非属性,能够监听新增 / 删除属性;
  • Proxy 能够间接监听数组的变动;
  • Proxy 有多达 13 种拦挡办法, 不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;
  • Proxy 返回的是一个新对象, 咱们能够只操作新的对象达到目标, 而 Object.defineProperty 只能遍历对象属性间接批改;

有余

  • 兼容性问题,而且无奈应用 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>

当浏览器读到这些代码时,会解析为对应的 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 文档'
                )
            ]
        )
    }
})

下面办法,会生成一个 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"}
            }
        }
    ]
}

首次渲染的时候,这个 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
}

不被举荐应用,在 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))

执行

# 编译
node template.js

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

{ 
    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: []}

应用 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;
}

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() 是更新后的回调函数,在 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>

源码剖析

定义: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;})
    }
}

监听变动: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)
    }
}

队列监听: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)
        }
    }
}

正文完
 0