Vue 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助高效地开发用户界面。
Vue 的两个核心功能:
声明式渲染:Vue 基于标准 HTML 拓展了一套模板语法,使得可以声明式地描述最终输出的 HTML 和 JavaScript 状态之间的关系。
响应性:Vue 会自动跟踪 JavaScript 状态并在其发生变化时响应式地更新 DOM。
核心概念
响应式系统
Vue 最标志性的功能就是其低侵入性的响应式系统。组件状态都是由响应式的 JavaScript 对象组成的。当更改它们时,视图会随即自动更新。
本质上,响应性是一种可以使声明式地处理变化的编程范式。
let a = 1
let b = update() // 2
function update() {
b = a + 1
}
a = 2
响应式系统要做的就是当变量 a 的值变化时,让变量 b 也自动更新。
原生 JavaScript 没有提供任何机制能做到这一点,但是,可以通过追踪对象属性的读写来实现。
在 JavaScript 中有两种劫持 property 访问的方式:getter / setters 和 Proxies。Vue 2 使用 getter / setters 完全是出于支持旧版本浏览器的限制;而 Vue 3 则使用了 Proxy 来创建响应式对象,仅将 getter / setter 用于 ref。
defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。还允许修改对象的属性的属性描述符。
Object.defineProperty(obj, prop, descriptor)
descriptor 是属性描述符对象,有以下属性:
configurable:布尔值,默认值:false,表示该属性的描述符是否能够被改变。
enumerable:布尔值,默认值:false,表示该属性是否出现在对象的枚举属性中。
writable(数据描述符): 布尔值,默认值:false,表示属性的值是否能被修改。
get(存取描述符): 属性的 getter 函数,当访问该属性时,会调用此函数,返回值会被用作属性的值,默认值:undefined。
set(存取描述符): 属性的 setter 函数,当属性值被修改时,会调用此函数,默认值:undefined。
const obj = {
a: 1,
b: null
}
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
get: function () {
return val;
},
set: function (newVal) {
obj.b = newVal + 1
console.log(obj.b)
return newVal
}
});
}
defineReactive(obj, 'a', obj.a)
obj.a = 10
Object.defineProperty() 是 Vue 2.x 的核心。如果浏览器不支持 Object.defineProperty,比如,IE8,那就等于不支持 Vue 2.x 框架。
Virtual DOM
虚拟节点(Virtual DOM)本质上就是一棵以 JavaScript 对象(VNode 节点)作为基础的树,使用 JavaScript 对象来描述 DOM 的结构,实际上它只是一层对真实 DOM 的抽象。应用的各种状态变化首先作用于 Virtual DOM,最终映射到 DOM。
<div class="a" id="b">我是内容</div>
{
tag:'div', // 元素标签
attrs:{ // 属性
class:'a',
id:'b'
},
text:'我是内容', // 文本内容
children:[] // 子元素
}
Virtual DOM 在 Vue 的底层实现上,借鉴了 Snabbdom(Virtual DOM 的一种简单实现,包括模块机制、钩子函数、diff 算法),将模板编译成 Virtual DOM 渲染函数,结合 Vue 自身的一些特性,如响应式系统、指令等。
Vue 是数据驱动视图的,数据发生变化视图就要随之更新,在更新视图的时候难免要操作 DOM,而操作真实 DOM 又是非常耗费性能的,这是因为浏览器的标准就把 DOM 设计的非常复杂。
而 JavaScript 的运算通常是比较快的,可以用 JavaScript 的计算性能来换取操作 DOM 所消耗的性能。当数据发生变化时,对比变化前后的虚拟节点,通过 DOM-Diff 算法计算出需要更新的地方,然后只更新需要更新的视图。这样就可以尽可能少的操作DOM。
这就是虚拟节点产生的原因以及最大的用途。
Vue 1.x 采用的是 DocumentFragment;而 Vue 2.x 借鉴 React 的 Virtual DOM,引入后,渲染速度提升了2~4倍,并大大降低了内存消耗。
DOM-Diff
前面提到,「当数据发生变化时,对比变化前后的虚拟节点,通过 DOM-Diff 算法计算出需要更新的地方」,以达到尽可能少的操作DOM的目的。
在 Vue 中,把 DOM-Diff 过程叫做 patch 过程。patch,意为“补丁”,即指对旧的 VNode 修补,打补丁从而得到新的 VNode。其本质就是把对比新旧两份 VNode 的过程:以新 VNode 为基准,改造旧 VNode 使之成为跟新 VNode 一样。所谓旧 VNode 就是数据变化之前视图所对应的虚拟节点,而新 VNode 是数据变化之后将要渲染的新的视图所对应的虚拟节点。
patch 过程做的其实就是事情:
创建节点:新 VNode 有而旧 VNode 没有的节点,就在旧 VNode 中创建。
删除节点:新 VNode 没有而旧 VNode 有的节点,就在旧 VNode 中删除。
更新节点:新旧 VNode 都有的节点,就以新 VNode 为准,更新旧 VNode。
DOM-Diff 算法采用的是深度优先遍历,把树形结构按照层级分解,只对同级的树节点进行比较,而不是对整个树进行逐层搜索遍历的方式。它先对比父节点是否相同,然后对比子节点是否相同,相同的话对比孙子节点是否相同。所以时间复杂度只有 O(n),是一种相当高效的算法。
DOM-Diff 算法的原理:逐个遍历新虚拟树的子节点,找到它在旧虚拟树中的位置,如果找到了就移动对应的节点,如果没找到说明是新增节点,则新建一个节点插入。遍历完成之后,如果旧虚拟树中还有没处理过的节点,则说明这些节点不需要了,删除它们即可。
观察者模式
观察者模式一般至少有一个可被观察的对象 Subject ,可以有多个观察者去观察这个对象。当被观察对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。
二者的关系是通过被观察者主动建立的,被观察者至少要有三个方法——添加观察者、移除观察者、通知观察者。当被观察者将某个观察者添加到自己的观察者列表后,观察者与被观察者的关联就建立起来了。此后只要被观察者在某种时机触发通知观察者方法时,观察者即可接收到来自被观察者的消息。
优点: 响应式。目标变化就会通知观察者,这是观察者最大的有点,也是因为这个优点,观察者模式在前端才会这么出名。
缺点: 不灵活。相比订阅发布模式,由于目标和观察者是耦合在一起的,所以观察者模式需要同时引入目标和观察者才能达到响应式的效果;而订阅发布模式只需要引入事件中心,订阅者和发布者可以不再一处。
// 被观察者
class Subject {
constructor() {
this.obs = [];
}
add(ob) {
this.obs.push(ob);
}
remove(ob) {
this.obs = this.obs.filter(o => o.name !== ob.name);
}
notify(message) {
this.obs.forEach(ob => ob.notified(message));
}
}
// 观察者
class Observer {
constructor(name) {
this.name = name;
}
notified(message) {
console.log(`Hellow, ${this.name}. This is ${message}!`);
}
}
const subject = new Subject();
const observerLi = new Observer('Li');
const observerZhao = new Observer('Zhao');
subject.add(observerLi);
subject.add(observerZhao);
subject.notify('Subject messagge 01');
subject.remove(observerLi);
subject.notify('Subject messagge 02');
渲染流程
初始化及挂载
在 new Vue() 之后。 Vue 会调用 _init 函数进行初始化。它会初始化生命周期、事件、 props、 methods、 data、 computed 与 watch 等。其中最重要的是通过 Object.defineProperty 设置 setter 与 getter 函数,用来实现「响应式」以及「依赖收集」。
初始化之后,调用 $mount 会挂载组件。如果是运行时编译,即不存在 render 函数,但是存在 template 的情况,需要进行「编译」步骤。
编译
编译是 template 到 render 函数的过程,可以分成 parse、optimize 与 generate 三个阶段:
parse:用正则等方式解析 template 模板中的指令、class、style 等数据,形成 AST。
optimize:标记 static 静态节点,这是 Vue 在编译过程中的一处优化,后面当 update 更新界面时,会有一个 patch 的过程, Diff 算法会直接跳过静态节点,从而减少了比较的过程,优化了 patch 的性能。
generate:将 AST 转化成渲染函数字符串的过程,得到结果是 render 函数和 staticRenderFns 函数数组的字符串表达。
generate 生成的字符串,最终会用 new Function 转换为相应的函数。
渲染
渲染是 render 函数到 DOM 的过程。
render 函数会在挂载阶段被调用。因为会读取所需对象的值,所以会触发 getter 函数进行「依赖收集」,「依赖收集」的目的是将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中。
在修改对象的值的时候,会触发对应的 setter, setter 通知之前「依赖收集」得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher 就会开始调用 update 来更新视图,当然这中间还有一个 patch 的过程以及使用队列来异步更新的策略。
源码解读
<!-- Vue.js v2.7.14 -->
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script id="content" type="x-template">
<div @click="onClickAlert">
<p>{{vueVersion}}</p>
<p>{{message}}</p>
</div>
</script>
<script>
const app = new Vue({
template: '#content',
props: {
name: String
},
propsData: {
name: 'Lizhao'
},
setup: function () {
return {
counter: 100
}
},
provide: {
createTime: new Date(2022, 10, 11, 9, 0, 0).toString()
},
computed: {
vueVersion () {
return `Vue@${this.version}`
}
},
watch: {
message: [
function () { console.log('message is changed! 01') },
function () { console.log('message is changed! 02') },
]
},
data: function () {
const { name } = this
return {
version: Vue.version,
message: `Hellow, ${name}. This is ${new Date()}.`
}
},
methods: {
onClickAlert () {
this.message = `Hellow, ${name}. This is ${new Date()}.`
}
}
})
app.$mount('#app')
</script>
new Vue()
每个 Vue 应用都是通过用 Vue 函数创建一个新的 Vue 实例开始的。new Vue() 根据传入的选项对象生成一个 Vue 实例。
const app = new Vue({
template: '#content',
data: {
message: `Hellow, Lizhao. This is ${new Date()}.`
}
})
初始化(_init)
Vue 实例化后,调用 this._init 方法来初始化实例对象。
Vue 本质上是一个函数。该函数声明后,会在其原型上扩展一些方法,比如,_init、$mount、__patch__
、$set、$delete、$watch、$on、$once、$off、$emit、_update
、$forceUpdate、$destroy、$nextTick 等方法,以及 $data、$props 等属性,同时,也会注册 compile、use、mixin、extend、util 等静态方法或属性。
function Vue(options) {
// ...
this._init(options);
}
initMixin$1(Vue);
function initMixin$1(Vue) {
Vue.prototype._init = function (options) {};
}
Vue.prototype.$mount = function (el, hydrating) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating);
};
初始化流程:
在 vue 实例上创建一些空对象,比如:$children、$refs、_events
、$slots、$attrs、$listeners、_provided
等;
再调用 beforeCreate 钩子、初始化实例的状态(比如:_props
、setup 选项、methods 中的方法、data 中的数据、computed 观察器、Watcher 观察器)、注入 _provided
数据、调用 beforeCreate 钩子;
function initMixin$1(Vue) {
Vue.prototype._init = function (options) {
//...
initProxy(vm);
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook$1(vm, 'beforeCreate', undefined, false /* setContext */);
initInjections(vm); // resolve injections before data/props
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook$1(vm, 'created');
// ...
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
}
function initState(vm) {
var opts = vm.$options;
if (opts.props)
initProps$1(vm, opts.props);
initSetup(vm);
if (opts.methods)
initMethods(vm, opts.methods);
if (opts.data) {
initData(vm);
} else {
var ob = observe((vm._data = {}));
ob && ob.vmCount++;
}
if (opts.computed)
initComputed$1(vm, opts.computed);
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
挂载($mount)
$mount 方法中最重要的两个方法:compileToFunctions,将 template 解析为一个渲染函数 render;mountComponent 将 render 函数的返回结果挂载到 DOM 节点。
Vue.prototype.$mount = function (el, hydrating) {
// ...
return mountComponent(this, el, hydrating);
};
var mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (el, hydrating) {
// ...
var _a = compileToFunctions(template, {
// ...
}, this), render = _a.render, staticRenderFns = _a.staticRenderFns;
options.render = render;
// ...
return mount.call(this, el, hydrating);
};
compileToFunctions 调用栈: compileToFunctions -> createCompiler -> createCompilerCreator -> createCompileToFunctionFn -> compileToFunctions -> compile -> baseCompile -> parse -> generate -> createFunction。
生成 render 函数的主要流程:
调用 parse 函数,将 template 或 el 选项的 DOM 节点,通过其 innerHTML 属性转换为字符串;
解析字符串转为 AST,并解析其属性、指令(v-for、v-if、v-once)等;
调用 generate 函数 将 AST 转换为生成函数的字符串,再转换成 render 函数。
var _a = createCompiler(baseOptions);
var compileToFunctions = _a.compileToFunctions;
var createCompiler = createCompilerCreator(function baseCompile(template, options) {
var ast = parse(template.trim(), options);
// ...
var code = generate(ast, options);
return {
ast: ast,
render: code.render,
staticRenderFns: code.staticRenderFns
};
});
function createCompilerCreator(baseCompile) {
return function createCompiler(baseOptions) {
function compile(template, options) {
// ...
var compiled = baseCompile(template.trim(), finalOptions);
{
detectErrors(compiled.ast, warn);
}
compiled.errors = errors;
compiled.tips = tips;
return compiled;
}
return {
compile: compile,
compileToFunctions: createCompileToFunctionFn(compile)
};
};
}
function createCompileToFunctionFn(compile) {
var cache = Object.create(null);
return function compileToFunctions(template, options, vm) {
// ...
var compiled = compile(template, options);
// ...
var res = {};
var fnGenErrors = [];
res.render = createFunction(compiled.render, fnGenErrors);
res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
return createFunction(code, fnGenErrors);
});
// ...
return (cache[key] = res);
};
}
function generate(ast, options) {
var state = new CodegenState(options);
var code = ast ? ast.tag === 'script' ? 'null' : genElement(ast, state) : '_c("div")';
return {
render: "with(this){return ".concat(code, "}"),
staticRenderFns: state.staticRenderFns
};
}
mountComponent 调用栈: mountComponent -> new Water -> updateComponent -> vm._render()
-> Vue.prototype._render
-> vm._update
-> Vue.prototype._update -> __patch__
-> Vue.prototype.__patch__
-> patch -> createPatchFunction -> createElm -> insert -> nodeOps.appendChild -> node.appendChild。
挂载流程:
new Watcher() 创建实例对象,并将 updateComponent 函数传入,Watcher 构造函数会执行该函数;
updateComponent 函数调用 vm._render
生成虚拟节点,再以虚拟节点为入参,调用 vm._update
方法;
vm._update
方法最终调用的是 patch 函数,该函数核心是 DOM-Diff 算法,计算出需要更新的 VNode,以达到尽量少的渲染节点的目的;
DOM-Diff 算法计算完成后,调用 createElm 或 patchVnode 函数更新节点,其本质上是调用原生节点的 node.appendChild、insertBefore 方法。
function mountComponent(vm, el, hydrating) {
// ...
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
// ...
new Watcher(vm, updateComponent, noop, watcherOptions, true);
// ...
}
var Watcher = (function () {
function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
// ...
if (isFunction(expOrFn)) {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
// ..
}
this.value = this.lazy ? undefined : this.get();
}
// ...
return Watcher;
}());
function lifecycleMixin(Vue) {
Vue.prototype._update = function (vnode, hydrating) {
// ...
if (!prevVnode) {
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);
} else {
vm.$el = vm.__patch__(prevVnode, vnode);
}
// ...
};
}
var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules$1 });
Vue.prototype.__patch__ = inBrowser ? patch : noop;
function createPatchFunction(backend) {
// ...
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
// ...
if (isDef(tag)) {
// ...
insert(parentElm, vnode.elm, refElm);
// ...
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text);
insert(parentElm, vnode.elm, refElm);
} else {
vnode.elm = nodeOps.createTextNode(vnode.text);
insert(parentElm, vnode.elm, refElm);
}
}
// ...
return function patch(oldVnode, vnode, hydrating, removeOnly) {
// ...
if (isUndef(oldVnode)) {
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {
var isRealElement = isDef(oldVnode.nodeType);
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
} else {
// ...
createElm(vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm));
// ...
}
}
// ...
};
}
function insert(parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (nodeOps.parentNode(ref) === parent) {
nodeOps.insertBefore(parent, elm, ref);
}
} else {
nodeOps.appendChild(parent, elm);
}
}
}
var nodeOps = Object.freeze({
// ...
insertBefore: insertBefore,
removeChild: removeChild,
appendChild: appendChild,
// ...
});
function appendChild(node, child) {
node.appendChild(child);
}
虚拟节点(VNode)
new VNode() 用一实例化一个虚拟节点,其调用栈:mountComponent -> vm._render
-> Vue.prototype._render
,最终返回一个 VNode 对象。
function renderMixin(Vue) {
// ...
Vue.prototype._render = function () {
// ...
var vnode;
try {
setCurrentInstance(vm);
currentRenderingInstance = vm;
vnode = render.call(vm._renderProxy, vm.$createElement);
}
// ...
return vnode;
};
}
render 函数的函数体长这样:
render.toString()
// function anonymous() {with(this){return _c('div',{on:{"click":onClickAlert}},[_c('p',[_v(_s(vueVersion))]),_v(" "),_c('p',[_v(_s(message))])])}}
function anonymous() {
with(this){
return _c(
'div',
{
on:{"click":onClickAlert}
},
[
_c(
'p',
[
_v(_s(vueVersion))]),
_v(" "),
_c('p',[_v(_s(message))
]
)
]
)
}
}
其中 with(this)
是指定语句块中的作用域,也就是 render.call(vm._renderProxy, vm.$createElement)
中的 vm._renderProxy
,_c、_v
就是 vm._renderProxy
下的方法。
initProxy = function initProxy(vm) {
if (hasProxy_1) {
// ...
vm._renderProxy = new Proxy(vm, handlers);
} else {
vm._renderProxy = vm;
}
};
_c
是一个简单的、返回 createElement$1 函数运行结果的函数,createElement$1 -> _createElement -> new VNode()。
function initRender(vm) {
// ...
vm._c = function (a, b, c, d) { return createElement$1(vm, a, b, c, d, false); };
vm.$createElement = function (a, b, c, d) { return createElement$1(vm, a, b, c, d, true); };
// ...
}
function createElement$1(context, tag, data, children, normalizationType, alwaysNormalize) {
// ...
return _createElement(context, tag, data, children, normalizationType);
}
function _createElement(context, tag, data, children, normalizationType) {
// ...
var vnode, ns;
if (typeof tag === 'string') {
var Ctor = void 0;
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
if (config.isReservedTag(tag)) {
// ...
vnode = new VNode(config.parsePlatformTagName(tag), data, children, undefined, undefined, context);
} else if ((!data || !data.pre) &&
// ...
vnode = createComponent(Ctor, data, context, children, tag);
} else {
// ...
vnode = new VNode(tag, data, children, undefined, undefined, context);
}
} else {
vnode = createComponent(tag, data, context, children);
}
// ...
}
VNode 是一个创建虚拟节点的构造函数:
var VNode = /** @class */ (function () {
function VNode(tag, data, children, text, elm, context, componentOptions, asyncFactory) {
this.tag = tag;
this.data = data;
this.children = children;
this.text = text;
this.elm = elm;
this.ns = undefined;
this.context = context;
// ...
}
Object.defineProperty(VNode.prototype, "child", {
// ...
});
return VNode;
}());
DOM-Diff(patch)
DOM-Diff 用对比变化前后的虚拟节点,计算出需要更新的节点,以达到尽可能少的操作DOM的目的。DOM-Diff 是在 patch 函数中实现的。
patch 函数的调用栈:mountComponent -> vm._update
-> Vue.prototype._update -> __patch__
-> Vue.prototype.__patch__
-> patch -> createPatchFunction。
patch 函数逻辑如下:
如果 vnode 不存在,但是 oldVnode 存在,则销毁 oldVnode;
如果 vnode 存在,但是 oldVnode 不存在,则创建 vnode;
如果 oldVnode 不是真实节点,且 vnode 和 oldVnode 的 key、tag 等相同,则调用 patchVnode 方法,即更新 oldVnode。
patchVnode 方法逻辑如下:
如果 oldVnode 和 vnode 指向同一节点,则不处理;
如果 VNode 和 oldVNode 均为静态节点,则不处理;
如果 vnode 有 text,则直接替换文本内容;
如果 oldVnode 和 vnode 都有 children,且两者 children 不是同一对象,则调用 updateChildren 方法;
如果 只有 vnode 都有 children,则调用 addVnodes 新增子节点;
如果 只有 oldVnode 都有 children,则调用 removeVnodes 删除子节点;
如果 vnode 没有 text,则将节点文本置为空字符。
var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules$1 });
function sameVnode(a, b) {
return (a.key === b.key &&
a.asyncFactory === b.asyncFactory &&
((a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)) ||
(isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error))));
}
function createPatchFunction(backend) {
// ...
function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
if (oldVnode === vnode) {
return;
}
// ...
var oldCh = oldVnode.children;
var ch = vnode.children;
// ...
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i)
cbs.update[i](oldVnode, vnode);
if (isDef((i = data.hook)) && isDef((i = i.update)))
i(oldVnode, vnode);
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) {
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
}
} else if (isDef(ch)) {
checkDuplicateKeys(ch);
if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '');
}
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text);
}
// ...
}
// ...
return function patch(oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode))
invokeDestroyHook(oldVnode);
return;
}
var isInitialPatch = false;
var insertedVnodeQueue = [];
if (isUndef(oldVnode)) {
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {
var isRealElement = isDef(oldVnode.nodeType);
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
} else {
// ...
createElm(vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm));
// ...
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm;
};
}
特别注意: cbs.update[i](oldVnode, vnode)
包含 updateAttrs、updateClass、updateDOMListeners、updateDOMProps、updateStyle 等函数的执行。这是因为,当虚拟节点是可 "修补" 的时,需要根据 VNode 的 data 数据,更新 oldVnode 下的真实 DOM 的属性、类名、样式、事件监听器等。
updateChildren 函数用于更新子节点,内部算法是为了复用一些没有变化的元素:
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
// ...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
canMove &&
nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
canMove &&
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
if (isUndef(oldKeyToIdx)) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
if (isUndef(idxInOld)) {
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
} else {
vnodeToMove = oldCh[idxInOld];
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldCh[idxInOld] = undefined;
canMove &&
nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
} else {
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx);
}
}
响应式实现
响应式的核心原理是 Object.defineProperty,核心函数是 defineReactive,其实现主要依赖三个构造函数:Observer、Dep、Watcher。
defineReactive 函数首先会实例化一个 Dep 对象,在读取数据时,get 方法会将 Dep.target(一个 Watcher 对象)存放到 Dep 对象的 subs 中;在更新数据时,set 方法会调用 Dep 对象的 notify 方法通知它内部所有的依赖执行 update。
注意: 只有触发 get 方法,Dep.target 才会被收集,也就是说,如果该属性没有被用到,则不收集到 subs。
function defineReactive(obj, key, val, customSetter, shallow, mock) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
// ...
// 默认对子属性也进行响应式绑定
var childOb = !shallow && observe(val, false, mock);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 当读取属性时,将 Dep.target 添加到 Dep 的依赖收集中
var value = getter ? getter.call(obj) : val;
// Dep.target 主要是的 Watch 函数的 get 方法中赋值的。当然,其他方也有
if (Dep.target) {
dep.depend({
target: obj,
type: "get" /* TrackOpTypes.GET */,
key: key
});
if (childOb) {
childOb.dep.depend();
if (isArray(value)) {
dependArray(value);
}
}
}
return isRef(value) && !shallow ? value.value : value;
},
set: function reactiveSetter(newVal) {
// ...
childOb = !shallow && observe(newVal, false, mock);
dep.notify({
type: "set" /* TriggerOpTypes.SET */,
target: obj,
key: key,
newValue: newVal,
oldValue: value
});
});
return dep;
}
Observer 构造函数其实是对 defineReactive 函数封装。它接收一个对象,并对对象中的每个属性调用 defineReactive 函数,深度遍历每一项,定义每个子对象中属性的 get 和 set 方法。
var Observer = (function () {
function Observer(value, shallow, mock) {
// ...
if (isArray(value)) {} else {
// ...
var keys = Object.keys(value);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
defineReactive(value, key, NO_INIITIAL_VALUE, undefined, shallow, mock);
}
}
}
// ...
return Observer;
}());
function observe(value, shallow, ssrMockReactivity) {
// ...
if (/** ... **/) {
return new Observer(value, shallow, ssrMockReactivity);
}
}
Dep 构造函数定义的是观察者模式中的被观察者类。它很简单,主要工作是维护一个依赖数组,提供添加、删除以及通知各依赖的方法。
var Dep = (function () {
function Dep() {
this._pending = false;
this.id = uid$2++;
this.subs = [];
}
Dep.prototype.addSub = function (sub) {
this.subs.push(sub);
};
Dep.prototype.removeSub = function (sub) {
this.subs[this.subs.indexOf(sub)] = null;
if (!this._pending) {
this._pending = true;
pendingCleanupDeps.push(this);
}
};
Dep.prototype.depend = function (info) {
if (Dep.target) {
Dep.target.addDep(this);
if (info && Dep.target.onTrack) {
Dep.target.onTrack(__assign({ effect: Dep.target }, info));
}
}
};
Dep.prototype.notify = function (info) {
if (!config.async) {
subs.sort(function (a, b) { return a.id - b.id; });
}
for (var i = 0, l = subs.length; i < l; i++) {
var sub = subs[i];
if (info) {
sub.onTrigger &&
sub.onTrigger(__assign({ effect: subs[i] }, info));
}
sub.update();
}
};
return Dep;
}());
Dep.target = null;
var targetStack = [];
function pushTarget(target) {
targetStack.push(target);
Dep.target = target;
}
function popTarget() {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
Watcher 构造函数定义的是观察者模式中的观察者类:添加依赖到 subs, 以及提供 update 函数。
var uid$1 = 0;
var Watcher = /** @class */ (function () {
function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
// ...
this.value = this.lazy ? undefined : this.get();
}
Watcher.prototype.get = function () {
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
}
// ...
finally {
if (this.deep) { traverse(value); }
popTarget();
this.cleanupDeps();
}
return value;
};
Watcher.prototype.addDep = function (dep) { /** ... **/};
Watcher.prototype.cleanupDeps = function (dep) { /** ... **/};
Watcher.prototype.update = function () {
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
Watcher.prototype.run = function () {
if (this.active) {
var value = this.get();
// ...
}
};
// ...
return Watcher;
}());
总结来说(以 render 函数为例):defineReactive 给组件的 data 选项的所有属性绑定 get 和 set 存取器;调用 render 函数时,先实例化一个 Watcher 对象,该对象在 get 方法中调用 render 函数,并将自己赋值给全局变量 Dep.target;如果 render 函数对 data 有读值操作,则将 Dep.target 添加到 subs;如果 render 函数有写值操作,则通过 dep.notify 通知 subs 下所有子元素执行 update 方法,即 Watcher 实例的 update 方法,重新调用 render 函数。
响应式数据绑定
data 选项的响应性初始化:Vue -> this._init -> initState -> initData:
function initData(vm) {
var data = vm.$options.data;
// ...
var ob = observe(data);
ob && ob.vmCount++;
}
computed 选项初始化,添加监听器到 vm._computedWatchers
:Vue -> this._init -> initState -> initComputed$1:
function initComputed$1(vm, computed) {
var watchers = (vm._computedWatchers = Object.create(null));
// ...
for (var key in computed) {
var userDef = computed[key];
var getter = isFunction(userDef) ? userDef : userDef.get;
if (!isSSR) {
watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions);
}
if (!(key in vm)) {
defineComputed(vm, key, userDef);
}
// ...
}
}
water 选项初始化:Vue -> this._init -> initState -> initWatch:
function initWatch(vm, watch) {
for (var key in watch) {
var handler = watch[key];
if (isArray(handler)) {
for (var i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
createWatcher(vm, key, handler);
}
}
}
function createWatcher(vm, expOrFn, handler, options) {
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
if (typeof handler === 'string') {
handler = vm[handler];
}
return vm.$watch(expOrFn, handler, options);
}
function stateMixin(Vue) {
// ...
Vue.prototype.$watch = function (expOrFn, cb, options) {
// ...
var watcher = new Watcher(vm, expOrFn, cb, options);
// ...
};
}
一个简单的 Vue
根据 Vue 的思路,模拟一个简单的响应式系统。
// index.js
function MyVue(options) {
this.$options = options
this.template = null
this.methods = {}
this.init()
}
MyVue.prototype.init = function () {
const vm = this
vm.template = document.querySelector(vm.$options.template)
vm.methods = vm.$options.methods
const data = vm.$options.data()
const keys = Object.keys(data)
keys.forEach(k => {
defineReactive(vm, k, data[k])
})
}
MyVue.prototype.$mount = function (selector) {
const vm = this
new Watcher(this, () => {
render(selector, vm)
})
}
window.MyVue = MyVue
<div id="app"></div>
<script src="./index.js"></script>
<script id="content" type="x-template">
<div @click="onClickMessage">
{{message}}
</div>
</script>
<script>
const app = new MyVue({
template: '#content',
data: function () {
return {
message: `Hellow, Lizhao. This is ${new Date()}.`
}
},
methods: {
onClickMessage () {
this.message = `Hellow, Lizhao. This is ${new Date()}.`
}
}
})
app.$mount('#app')
</script>
响应式系统应该实现以下三点:
data 改变时,触发视图改变,即自动调用渲染函数。
function defineReactive(obj, key, val) {
var dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
if (Dep.target) {
dep.depend();
}
return val
},
set: function reactiveSetter(newVal) {
val = newVal
dep.notify();
}
});
return dep;
}
function render (selector, inst) {
const dom = parseToDom(inst.template.innerHTML, inst);
const $app = document.querySelector(selector);
$app.innerHTML = '';
$app.appendChild(dom)
}
function parseToDom (html, inst) {
const dom = new DOMParser().parseFromString(html, 'text/html').body.childNodes[0]
dom.innerText = dom.innerText.replace(/{{.*}}/, function (str) {
const key = str.replace(/{{(.*)}}/, '$1')
return inst[key]
})
const methodName = dom.getAttribute('@click')
dom.addEventListener('click', function () {
inst.methods[methodName].bind(inst)()
}, false)
return dom
}
var Dep = (function () {
function Dep() {
this.subs = [];
}
Dep.prototype.addSub = function (sub) {
this.subs.push(sub);
};
Dep.prototype.depend = function () {
if (Dep.target) {
this.addSub(Dep.target)
}
}
Dep.prototype.removeSub = function (sub) {
this.subs = this.subs.filter((item) => sub !== item)
};
Dep.prototype.notify = function () {
this.subs.forEach((sub) => { sub.update() })
};
return Dep;
}());
Dep.target = null;
var Watcher = (function () {
function Watcher(vm, expOrFn) {
this.deps = []
this.vm = vm
this.getter = expOrFn
this.value = this.get()
}
Watcher.prototype.get = function () {
Dep.target = this
const value = this.getter.call(this.vm)
Dep.target = null
return value;
};
Watcher.prototype.addDep = function (dep) {
this.deps.push(dep)
};
Watcher.prototype.update = function () {
const value = this.getter.call(this.vm)
return value
};
return Watcher;
}());
相关问题
为什么说 Vue 没有完全遵循 MVVM 模型?
MVVM(Model–view–viewmodel)模型的定义:
MVVM 由 3 个部分组成:Model(模型)是数据和逻辑;View(视图)是用户在屏幕上看到的结构、布局和外观,也称UI; ViewModel(视图模型)是一个绑定器,能和 View 层和 Model 层进行通信。
MVVM 的核心实现是由 ViewModel 层数据绑定,核心思想是分离,即通过 ViewModel 让 View 层和 Model 层解耦。
严格的 MVVM 要求 View 不能和 Model 直接通信,而 Vue 在组件提供了 $refs 这个属性,让 Model 可以直接操作 View,违反了这一规定,所以说 Vue 没有完全遵循 MVVM。
为什么要使用 Virtual DOM ?
具备跨平台的优势:由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。
运行效率高:DOM 操作的执行速度远不如 Javascript 的运算速度快。因此,把大量的 DOM 操作转为 Javascript 计算,运用 DOM-Diff 算法计算出真正需要更新的节点,最大限度地减少 DOM 操作,从而显著提高性能。
提升渲染性能:Virtual DOM 的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。
注意: 关于性能方面,在首次渲染的时候肯定不如直接操作 DOM,因为要维护一层额外的 Virtual DOM。但如果后续有频繁操作 DOM 的操作,就可能会有性能的提升。
key 在虚拟节点中的作用?
key 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。
key 作用的原理是:Vue 在 patch 过程中通过 key 可以精准判断两个节点是否是同一个,从而避免频繁更新不同元素,使得整个 patch 过程更加高效,减少 Dom 操作量,提高性能。
另外,若不设置 key 还可能在列表更新时候引发一些隐藏的 bug;key 也可以用于强制替换元素/组件而不是重复使用它,比如,完整地触发组件的生命周期钩子、触发过渡。
有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。
参考资料
Vue 2.x 中文文档
Vue 官网 - 深入响应式系统
稀土掘金 - 剖析 Vue.js 内部运行机制
Vue中文社区 - Vue源码系列
Github - Vue.js源码分析
稀土掘金 - Vue响应式原理和实现方式,Observer、Dep、Watcher源码解析(下),深入浅出
Vue源码解析 - 从零构建一个响应式系统