共计 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 会针对模板中的各种标签、指令、事件进行提取拆分,别离解决。
组件渲染与更新
首次渲染
- 解析模板为 render 函数(或在开发环境已实现,vue-loader)
- 触发响应式,监听 data 属性 getter setter
- 执行 render 函数,生成 vnode
- path(elem, vnode)
更新过程
- 批改 data,触发 setter(此前在 getter 中已被监听)
- 从新执行 render 函数,生成 newVnode
- 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)
}
}
}