学习Vue---5.Vue源码分析

vue

分析 vue 作为一个MVVM 框架的基本实现原理

1. [].slice.call(lis): 将伪数组转换为真数组

    //1. [].slice.call(lis): 根据伪数组生成对应的真数组

const lis = document.getElementsByTagName('li') // lis是伪数组(是一个特别的对象, length和数值下标属性)

console.log(lis instanceof Object, lis instanceof Array)

// 数组的slice()截取数组中指定部分的元素, 生成一个新的数组 [1, 3, 5, 7, 9], slice(0, 3)

// slice2()

Array.prototype.slice2 = function (start, end) {

start = start || 0

end = start || this.length

const arr = []

for (var i = start; i < end; i++) {

arr.push(this[i])

}

return arr

}

const lis2 = Array.prototype.slice.call(lis) // lis.slice()

console.log(lis2 instanceof Object, lis2 instanceof Array)

// lis2.forEach()

2. node.nodeType: 得到节点类型

节点:document(html文件节点)、Element(元素节点)、Attribute(属性节点)、Text(文本节点)

    //2. node.nodeType: 得到节点类型

const elementNode = document.getElementById('test')

const attrNode = elementNode.getAttributeNode('id')

const textNode = elementNode.firstChild

console.log(elementNode.nodeType, attrNode.nodeType, textNode.nodeType) # 1 2 3

3. Object.defineProperty(obj, propertyName, {}): 给对象添加属性(指定描述符)

    //3. Object.defineProperty(obj, propertyName, {}): 给对象添加属性(指定描述符)

const obj = {

firstName: 'A',

lastName: 'B'

}

//obj.fullName = 'A-B'

Object.defineProperty(obj, 'fullName', {

// 属性描述符:

// 数据描述符

//访问描述符

// 当读取对象此属性值时自动调用, 将函数返回的值作为属性值, this为obj

get() {

return this.firstName + "-" + this.lastName

},

// 当修改了对象的当前属性值时自动调用, 监视当前属性值的变化, 修改相关的属性, this为obj

set(value) {

const names = value.split('-')

this.firstName = names[0]

this.lastName = names[1]

}

})

console.log(obj.fullName) // A-B

obj.fullName = 'C-D'

console.log(obj.firstName, obj.lastName) // C D

Object.defineProperty(obj, 'fullName2', {

configurable: false, //是否可以重新define

enumerable: true, // 是否可以枚举(for..in / keys())

value: 'A-B', // 指定初始值

writable: false // value是否可以修改

})

console.log(obj.fullName2) // A-B

obj.fullName2 = 'E-F'

console.log(obj.fullName2) // A-B

/*Object.defineProperty(obj, 'fullName2', {

configurable: true,

enumerable: true,

value: 'G-H',

writable: true

})*/

4. Object.keys(obj): 得到对象自身可枚举属性组成的数组

//4. Object.keys(obj): 得到对象自身可枚举属性组成的数组

const names = Object.keys(obj)

console.log(names)

5. obj.hasOwnProperty(prop): 判断prop是否是obj自身的属性

//5. obj.hasOwnProperty(prop): 判断prop是否是obj自身的属性

console.log(obj.hasOwnProperty('fullName'), obj.hasOwnProperty('toString')) // true false

6. DocumentFragment: 文档碎片(高效批量更新多个节点)

/6. DocumentFragment: 文档碎片(高效批量更新多个节点)

// document: 对应显示的页面, 包含n个elment 一旦更新document内部的某个元素界面更新

// documentFragment: 内存中保存n个element的容器对象(不与界面关联), 如果更新fragment中的某个element, 界面不变

// DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。它被作为一个轻量版的 Document 使用,就像标准的document一样,

// 存储由节点(nodes)组成的文档结构。与document相比,最大的区别是DocumentFragment 不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。

/*

<ul >

<li>test1</li>

<li>test2</li>

<li>test3</li>

</ul>

*/

const ul = document.getElementById('fragment_test')

// 1. 创建fragment

const fragment = document.createDocumentFragment()

// 2. 取出ul中所有子节点取出保存到fragment

let child

while (child = ul.firstChild) { // 一个节点只能有一个父亲

fragment.appendChild(child) // 先将child从ul中移除, 添加为fragment子节点

}

// 3. 更新fragment中所有li的文本

fragment.childNodes.forEach(node => {

console.log('node', node)

if (node.nodeType === 1) { // 元素节点 <li>

node.textContent = 'niuxiaofu hi'

}

})

// 4. 将fragment插入ul

ul.appendChild(fragment)

console.log('ul.childNodes', ul.childNodes)

console.log('ul.children', ul.children)

二、数据代理

1.数据代理

通过一个对象代理对另一个对象(在前一个对象内部)中属性的操作(读/写)

2.vue 数据代理

data 对象的所有属性的操作(读/写)由 vm 对象来代理操作

3.好处

通过 vm 对象就可以方便的操作 data 中的数据

4.基本实现流程

  1. 通过 Object.defineProperty() 给 vm 添加与 data 对象的属性对应的属性描述符
  2. 所有添加的属性都包含 getter/setter
  3. getter/setter 内部去操作 data 中对应的属性数据

三、模板解析

1.模板解析的基本流程

1) 将 el 的所有子节点取出,添加到一个新建的文档 fragment 对象中

2) 对 fragment 中的所有层次子节点递归进行编译解析处理

  1. 对大括号表达式文本节点进行解析

  2. 对元素节点的指令属性进行解析

    1. 事件指令解析

    2. 一般指令解析

3) 将解析后的 fragment 添加到 el 中显示

2.大括号表达式解析

  1. 根据正则对象得到匹配出的表达式字符串:子匹配/RegExp.$1 name
  2. 从 data 中取出表达式对应的属性值
  3. 将属性值设置为文本节点的 textContent

3.事件指令解析

  1. 从指令名中取出事件名
  2. 根据指令的值(表达式)从 methods 中得到对应的事件处理函数对象
  3. 给当前元素节点绑定指定事件名和回调函数的 dom 事件监听
  4. 指令解析完后,移除此指令属性

4.一般指令解析

1) 得到指令名和指令值(表达式) text/html/class msg/myClass

2) 从 data 中根据表达式得到对应的值

3) 根据指令名确定需要操作元素节点的什么属性

  1. v-text---textContent 属性
  2. v-html---innerHTML 属性
  3. v-class--className 属性

4) 将得到的表达式的值设置到对应的属性上

5) 移除元素的指令属性

四、数据绑定

1.数据绑定

一旦更新了 data 中的某个属性数据,所有界面上直接使用或间接使用了此属性的节点都会更新。

2.数据劫持

  1. 数据劫持是 vue 中用来实现数据绑定的一种技术
  2. 基本思想:通过 defineProperty() 来监视 data 中所有属性(任意层次)数据的变化, 一旦变化就去更新界面

3.四个重要对象

实现数据的绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器 Observer,用来监听所有属性。

如果属性发生变化了,就需要告诉订阅者 Watcher 看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器 Dep 来专门收集这些订阅者,然后在监听器 Observer 和订阅 Watcher 之间进行统一管理。

接着,我们还需要有一个指令解析器 Compile,对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者 Watcher,并替换模板数据或者绑定相应的函数。此时当订阅者 Watcher 接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。

3.1 Observer(监听器)

  1. 用来对 data 所有属性数据进行劫持的构造函数
  2. 给 data 中所有属性重新定义属性描述(get/set)
  3. 为 data 中的每个属性创建对应的 dep 对象

function Observer(data) {

// 保存data对象

this.data = data;

// 走起,开始对data监视

this.walk(data);

}

Observer.prototype = {

walk: function(data) {

//将observer保存在me变量中

var me = this;

// 遍历data中所有属性

Object.keys(data).forEach(function(key) {

// 针对指定属性进行处理

me.convert(key, data[key]);

});

},

convert: function(key, val) {

// 对指定属性实现响应式数据绑定

this.defineReactive(this.data, key, val);

},

defineReactive: function(data, key, val) {

// 创建与当前属性对应的dep对象

var dep = new Dep();

// 间接递归调用实现对data中所有层次属性的劫持

var childObj = observe(val);

// 给data重新定义属性(添加set/get)

Object.defineProperty(data, key, {

enumerable: true, // 可枚举

configurable: false, // 不能再define

get: function() {

// 建立dep与watcher的关系

if (Dep.target) {

dep.depend();

}

// 返回属性值

return val;

},

set: function(newVal) { // 监视key属性的变化,目的是更新界面

if (newVal === val) {

return;

}

val = newVal;

// 新的值是object的话,进行监听

childObj = observe(newVal);

// 通知dep

dep.notify();

}

});

}

};

function observe(value, vm) {

// value必须是对象, 因为监视的是对象内部的属性

if (!value || typeof value !== 'object') {

return;

}

// 创建一个对应的观察者对象

return new Observer(value);

};

var uid = 0;

function Dep() {

// 标识属性

this.id = uid++;

// 相关的所有watcher的数组

this.subs = [];

}

Dep.prototype = {

// 添加watcher到dep中

addSub: function(sub) {

this.subs.push(sub);

},

// 建立dep与watcher之间的关系

depend: function() {

Dep.target.addDep(this);

},

removeSub: function(sub) {

var index = this.subs.indexOf(sub);

if (index != -1) {

this.subs.splice(index, 1);

}

},

notify: function() {

// 通知所有相关的watcher(订阅者)

this.subs.forEach(function(sub) {

sub.update();

});

}

};

Dep.target = null;

3.2 Dep(Depend)

  1. data 中的每个属性(所有层次)都对应一个 dep 对象
  2. 创建的时机:

    1. 在初始化 define data 中各个属性时创建对应的 dep 对象
    2. 在 data 中的某个属性值被设置为新的对象时

  3. 对象的结构:

    function Dep() {

    // 标识属性

    this.id = uid++; // 每个dep都有一个唯一的id

    // 相关的所有watcher的数组

    this.subs = []; //包含n个对应watcher的数组(subscribes的简写)

    }

    {

    this.id = uid++,

    this.subs = []

    }

  4. subs 属性说明

    1. 当 watcher 被创建时,内部将当前 watcher 对象添加到对应的 dep 对象的subs 中
    2. 当此 data 属性的值发生改变时,subs 中所有的 watcher 都会收到更新的通知,从而最终更新对应的界面

3.3 Compiler(指令解析器)

  1. 用来解析模板页面的对象的构造函数(一个实例)

  2. 利用 compile 对象解析模板页面

  3. 每解析一个表达式(非事件指令,如{{}}或v-text,v-html)都会创建一个对应的 watcher 对象,并建立 watcher 与 dep 的关系

  4. complie 与 watcher 关系:一对多的关系

function Compile(el, vm) {

// 保存vm

this.$vm = vm;

// 保存el元素

this.$el = this.isElementNode(el) ? el : document.querySelector(el);

// 如果el元素存在

if (this.$el) {

// 1. 取出el中所有子节点, 封装在一个framgment对象中

this.$fragment = this.node2Fragment(this.$el);

// 2. 编译fragment中所有层次子节点

this.init();

// 3. 将fragment添加到el中

this.$el.appendChild(this.$fragment);

}

}

Compile.prototype = {

node2Fragment: function (el) {

var fragment = document.createDocumentFragment(),

child;

// 将原生节点拷贝到fragment

while (child = el.firstChild) {

fragment.appendChild(child);

}

return fragment;

},

init: function () {

// 编译fragment

this.compileElement(this.$fragment);

},

compileElement: function (el) {

// 得到所有子节点

var childNodes = el.childNodes,

// 保存compile对象

me = this;

// 遍历所有子节点

[].slice.call(childNodes).forEach(function (node) {

// 得到节点的文本内容

var text = node.textContent;

// 正则对象(匹配大括号表达式)

var reg = /\{\{(.*)\}\}/; // {{name}}

// 如果是元素节点

if (me.isElementNode(node)) {

// 编译元素节点的指令属性

me.compile(node);

// 如果是一个大括号表达式格式的文本节点

} else if (me.isTextNode(node) && reg.test(text)) {

// 编译大括号表达式格式的文本节点

me.compileText(node, RegExp.$1); // RegExp.$1: 表达式 name

}

// 如果子节点还有子节点

if (node.childNodes && node.childNodes.length) {

// 递归调用实现所有层次节点的编译

me.compileElement(node);

}

});

},

compile: function (node) {

// 得到所有标签属性节点

var nodeAttrs = node.attributes,

me = this;

// 遍历所有属性

[].slice.call(nodeAttrs).forEach(function (attr) {

// 得到属性名: v-on:click

var attrName = attr.name;

// 判断是否是指令属性

if (me.isDirective(attrName)) {

// 得到表达式(属性值): test

var exp = attr.value;

// 得到指令名: on:click

var dir = attrName.substring(2);

// 事件指令

if (me.isEventDirective(dir)) {

// 解析事件指令

compileUtil.eventHandler(node, me.$vm, exp, dir);

// 普通指令

} else {

// 解析普通指令(v-model和v-class)

compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);

}

// 移除指令属性

node.removeAttribute(attrName);

}

});

},

compileText: function (node, exp) {

// 调用编译工具对象解析

compileUtil.text(node, this.$vm, exp);

},

isDirective: function (attr) {

return attr.indexOf('v-') == 0;

},

isEventDirective: function (dir) {

return dir.indexOf('on') === 0;

},

isElementNode: function (node) {

return node.nodeType == 1;

},

isTextNode: function (node) {

return node.nodeType == 3;

}

};

// 指令处理集合

var compileUtil = {

// 解析: v-text/{{}}

text: function (node, vm, exp) {

this.bind(node, vm, exp, 'text');

},

// 解析: v-html

html: function (node, vm, exp) {

this.bind(node, vm, exp, 'html');

},

// 解析: v-model

model: function (node, vm, exp) {

// 实现数据的初始化显示和创建对应watcher

this.bind(node, vm, exp, 'model');

var me = this,

// 得到表达式的值

val = this._getVMVal(vm, exp);

// 双向数据绑定

// 1.给节点绑定input事件监听(输入改变时触发)

node.addEventListener('input', function (e) {

// 得到输入的最新值

var newValue = e.target.value;

// 如果没有变化直接结束

if (val === newValue) {

return;

}

// 2.将最新的value保存给表达式对应的属性

me._setVMVal(vm, exp, newValue);

// 保存最新的值

val = newValue;

});

},

// 解析: v-class

class: function (node, vm, exp) {

this.bind(node, vm, exp, 'class');

},

// 真正用于解析指令的方法

bind: function (node, vm, exp, dir) {

/*实现初始化显示*/

// 根据指令名(text)得到对应的更新节点函数

var updaterFn = updater[dir + 'Updater'];

// 如果存在调用来更新节点

updaterFn && updaterFn(node, this._getVMVal(vm, exp));

// 为表达式创建对应的watcher对象,实现节点的更新显示

new Watcher(vm, exp, function (value, oldValue) {/*更新界面*/

// 当对应的属性值发生了变化时, 自动调用, 更新对应的节点

updaterFn && updaterFn(node, value, oldValue);

});

},

// 事件处理

eventHandler: function (node, vm, exp, dir) {

// 得到事件名/类型: click

var eventType = dir.split(':')[1],

// 根据表达式得到事件处理函数(从methods中): test(){}

fn = vm.$options.methods && vm.$options.methods[exp];

// 如果都存在

if (eventType && fn) {

// 绑定指定事件名和回调函数的DOM事件监听, 将回调函数中的this强制绑定为vm

node.addEventListener(eventType, fn.bind(vm), false);

}

},

// 得到表达式对应的value

_getVMVal: function (vm, exp) {

var val = vm._data;

exp = exp.split('.');

exp.forEach(function (k) {

val = val[k];

});

return val;

},

_setVMVal: function (vm, exp, value) {

var val = vm._data;

exp = exp.split('.');

exp.forEach(function (k, i) {

// 非最后一个key,更新val的值

if (i < exp.length - 1) {

val = val[k];

} else {

val[k] = value; // 触发data的set(又进入了数据绑定流程)

}

});

}

};

// 包含多个用于更新节点方法的对象

var updater = {

// 更新节点的textContent

textUpdater: function (node, value) {

node.textContent = typeof value == 'undefined' ? '' : value;

},

// 更新节点的innerHTML

htmlUpdater: function (node, value) {

node.innerHTML = typeof value == 'undefined' ? '' : value;

},

// 更新节点的className

classUpdater: function (node, value, oldValue) {

var className = node.className;

node.className = className + (className?' ':'') + value;

},

// 更新节点的value

modelUpdater: function (node, value, oldValue) {

node.value = typeof value == 'undefined' ? '' : value;

}

};

3.4 Watcher(订阅者)

  1. 模板中每个非事件指令或表达式都对应一个 watcher 对象(与模板中表达式[不包括事件指令]一一对应)
  2. 监视当前表达式数据的变化
  3. 创建的时机:初始化的解析大括号表达式/一般指令时创建

  4. 对象的组成

function Watcher(vm, exp, cb){

this.vm = vm; // vm 对象

this.exp = exp; // 对应指令的表达式

this.cb = cb; // 当表达式所对应的数据发生改变的回调函数

this.value = this.get(); // 表达式当前的值

this.depIds = {};

// 表达式中各级属性所对应的dep对象的集合对象

// 属性名为dep的id, 属性值为dep

}

function Watcher(vm, exp, cb) {

this.cb = cb; // callback

this.vm = vm;

this.exp = exp; // 表达式

this.depIds = {}; // {0: d0, 1: d1, 2: d2}包含所有相关的dep的容器对象

this.value = this.get(); // 保存表达式的初始值

}

Watcher.prototype = {

update: function () {

this.run();

},

run: function () {

// 得到最新的值

var value = this.get();

// 得到旧值

var oldVal = this.value;

// 如果不相同

if (value !== oldVal) {

this.value = value;

// 调用回调函数更新对应的界面

this.cb.call(this.vm, value, oldVal);

}

},

addDep: function (dep) {

// 判断dep与watcher的关系是否已经建立

if (!this.depIds.hasOwnProperty(dep.id)) {

// 将watcher添加到dep,用于更新

dep.addSub(this);

// 将dep添加到watcher,用于防止重复建立关系

this.depIds[dep.id] = dep;

}

},

get: function () {

// 给dep指定当前的watcher

Dep.target = this;

// 获取当前表达式的值, 内部会调用data属性的get(建立dep与watcher的关系)

var value = this.getVMVal();

// 去除dep中指定的当前watcher

Dep.target = null;

return value;

},

// 得到表达式的值

getVMVal: function () {

var exp = this.exp.split('.');

var val = this.vm._data; // data对象

exp.forEach(function (k) {

val = val[k]; // 触发表达式中有的属性的get(建立属性对应的dep与当前watcher的关系)

});

return val;

}

};

/*

const obj1 = {id: 1}

const obj12 = {id: 2}

const obj13 = {id: 3}

const obj14 = {id: 4}

const obj2 = {}

const obj22 = {}

const obj23 = {}

// 双向1对1

// obj1.o2 = obj2

// obj2.o1 = obj1

// obj1: 1:n

obj1.o2s = [obj2, obj22, obj23]

// obj2: 1:n

obj2.o1s = {

1: obj1,

2: obj12,

3: obj13

}

*/

总结:dep 与 watcher 的关系 --> 多对多

vm.name = 'abc'-->data中的name属性值变化-->name的set()调用-->dep-->相关的所有watcher-->cb()-->updater

  1. data 中的一个属性对应一个 dep,一个 dep 中可能包含多个 watcher(模板中有几个表达式使用到了同一个属性)【{{name}}/v-text="name"】

  2. 模板中一个非事件表达式对应一个 watcher,一个 watcher 中可能包含多个dep【多层表达式:a.b.c】

  3. 数据绑定使用到2个核心技术

    1. defineProperty()
    2. 消息订阅与发布

4.MVVM原理图分析

4.1 初始化阶段

MVVM 中会创建 Observer(用来劫持/监听所有属性)和 Compile(解析指令/大括号表达式),

Observer:要劫持就需要对应的set()方法,所以在observer中为每一个属性创建了一个 dep 对象(与 data 中的属性一一对应)

Compile:(做了两件事)

  1. 目的是初始化视图(显示界面),调用 updater(有很多更新节点的方法)
  2. 为表达式创建对应的 Watcher ,同时指定了更新节点的函数

Watcher 和 Dep 建立关系:

  1. watcher 放到 dep 中(添加订阅者),dep 中有一个 subs,是用来保存 n 个 watcher 的数组容器
  2. dep 放到 watcher 中,watcher 中的 depIds 是用来保存 n 个 dep 的对象容器。为了判断 dep 与 watcher 的关系是否已经建立(防止重复的建立关系)

以上都是初始化阶段会经历的过程

4.2 更新阶段

vm.name = 'Tom' 导致 data 中的数据变化,会触发监视 data 属性的 observer 中的 set() 方法,然会它又会通知 dep,dep 会去通知它保存的所有相关的 watcher,watcher 收到信息后,其回调函数会去调用 updater 更新界面

如下图所示:(黑线是初始化阶段,红线是更新阶段)

五、双向数据绑定

  1. 双向数据绑定是建立在单向数据绑定(model==>View)的基础之上的
  2. 双向数据绑定的实现流程:

    1. 在解析 v-model 指令时,给当前元素添加 input 监听
    2. 当 input 的 value 发生改变时,将最新的值赋值给当前表达式所对应的 data 属性

参考链接:

【1】Node - Web API 接口参考 | MDN

以上是 学习Vue---5.Vue源码分析 的全部内容, 来源链接: utcz.com/z/376103.html

回到顶部