vue-tutorial
vue-tutorial copied to clipboard
vue源码参考文档
剖析Vue原理&实现双向绑定MVVM Object.keys可以用来遍历数组或者对象的索引值或者属性值
var arr = ['a', 'b', 'c'];
console.log(Object.keys(arr)); // console: ['0', '1', '2']
// array like object
var obj = { 0: 'a', 1: 'b', 2: 'c' };
console.log(Object.keys(obj)); // console: ['0', '1', '2']
// array like object with random key ordering
var an_obj = { 100: 'a', 2: 'b', 7: 'c' };
console.log(Object.keys(an_obj)); // console: ['2', '7', '100']
// getFoo is property which isn't enumerable
var my_obj = Object.create({}, { getFoo: { value: function() { return this.foo; } } });
my_obj.foo = 1;
console.log(Object.keys(my_obj)); // console: ['foo']
Object.defineProperty Object.defineProperty接受三个参数,而且都是必填的,
- 第一个参数:目标对象
- 第二个参数:需要定义的属性或方法的名字
- 第三个参数:目标属性所拥有的特性
var obj= {}
Object.defineProperty(obj,"name",{
value:'wsscat'
})
console.log(obj.name);//wsscat
我们可以用set和get监听值的读取和修改,进而触发我们需要的操作
var obj = {}
Object.defineProperty(obj, "name", {
set: function (newValue) {
console.log('newValue:' + newValue)
},
get: function () {
console.log("getValue:")
return '你已经成功取值'
}
})
obj.name = 'wsscat' //log: newValue:wsscat
console.log(obj.name) //log: getValue:你已经成功取值
Observer
对对象的属性进行遍历并监听值的变化
下面代码我们封装一个observe方法,observe方法执行对obj对象的遍历,每个遍历我们对其属性添加一个defineProperty监听,要注意的是如果属性的值也是对象我们要再对子属性这个对象里面的属性值遍历监听,所以我们再observe方法写了一个判断是否为空和为对象的判断条件
var obj = {
age: 1,
skill: 'js',
name: 'autumns',
obj: {
num: 1
}
}
function observe(obj) {
//判断对象的自属性是否对象,如果时对象继续执行监听
if (!obj || typeof obj !== 'object') {
return;
}
Object.keys(obj).forEach(function (key) {
//console.log(key + '=>' + obj[key]);
//遍历监听
defineProperty(obj, key, obj[key]);
})
}
function defineProperty(obj, key, val) {
observe(val); //监听子属性,如果子属性也是对象,继续监听
Object.defineProperty(obj, key, {
set: function (newValue) {
console.log('newValue:' + newValue);
//设置新值
val = newValue;
},
get: function () {
console.log("getValue:" + val);
//返回val设置对应属性值
return val;
}
})
}
observe(obj); //对对象遍历并监听每个属性值的变化
obj.obj.num = 2; //log: newValue:2 监听子属性
obj.name = 'wsscat' //log: newValue:wsscat
console.log(obj.name) //log: getValue:你已经成功取值
Compile
这部分是解析模版指令,将模版中的变量变成数据,如同我们使用angular和vue中的表达式**{{变量}},我们就是把表达式里面的变量转化为数据 为了不想重复操作DOM,我们把el作用域下的DOM结构转换成文档碎片fragment**,并进行解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中,下面的node2Fragment就是实现这个保存DOM结构到文档碎片的功能
function node2Fragment(el) {
var fragment = document.createDocumentFragment(),
child;
// 将原生节点拷贝到fragment
while (child = el.firstChild) {
//console.log(el.firstChild)
//console.log(child)
fragment.appendChild(child);
}
return fragment;
}
解析表达式**{{name}}**,这里分三种情况
//compileElement分支
<p>
<span>{{name}}</span>
</p>
//compileText分支
{{my}}
//compile分支
<p v-abc="{{name}}" id="cba">1</p>
compileElement: function (el) {
var childNodes = el.childNodes,
me = this;
[].slice.call(childNodes).forEach(function (node) {
var text = node.textContent;
var reg = /\{\{(.*)\}\}/;
//<p>{name}<p>
if (me.isElementNode(node)) {
me.compile(node);
}
//{name}
else if (me.isTextNode(node) && reg.test(text)) {
me.compileText(node, RegExp.$1);
}
//<p><span>{{name}}</span></p>
if (node.childNodes && node.childNodes.length) {
me.compileElement(node);
}
});
}
Compile
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="demo">
<div v-text="name.a">{{name}}</div>
</div>
<script>
function compile(el, vm) {
// 选取节点
var $el = document.querySelector(el);
// 框架选项,比如data,methods等
var $vm = vm;
// 创建fragment保存html文档
var $fragment = document.createDocumentFragment();
// 一会儿再遍历真是DOMJ节点时候需要的容器
var child;
// 遍历文本节点和元素节点,并且把它重新赋给fragment
while (child = $el.firstChild) {
$fragment.appendChild(child);
}
// 第一次遍历整个$fragment,后面逐渐遍历里面的子元素
function compileElement(el) {
// 转化为childNodes数组进行是否元素节点和文本节点等判断
var childNodes = el.childNodes;
// 遍历childNodes,遍历之前先转化为真实数组,才可以使用foreach
[].slice.call(childNodes).forEach(function (node) {
// 获取节点的内容
var text = node.textContent;
// 正则寻找{{xxx}}的表达式
var reg = /\{\{(.*)\}\}/;
// 匹配这种节点<div>{{name}}</div>
if (node.nodeType == 1) {
console.log(1)
compile(node);
}
// 匹配这种节点{{name}},并判断是否含有{{xxx}}表达式
else if (node.nodeType == 3 && reg.test(text)) {
console.log(3, reg.test(text))
}
// 匹配这种节点<div><div>{{name}}</div></div>
if (node.childNodes && node.childNodes.length) {
console.log(4)
}
})
}
// 首次遍历整个id="demo"的标签闭合范围
compileElement($fragment);
// 通过遍历标签上的所有属性值,来进行对应的指令绑定
function compile(node) {
var nodeAttrs = node.attributes;
// 遍历标签上的所有属性值
[].slice.call(nodeAttrs).forEach(function (attr) {
console.log(attr)
var attrName = attr.name;
// 根据属性名筛选出所有包含v-前缀的指令
if (attrName.indexOf('v-') == 0) {
console.log("v")
// v-model="name" => name
var exp = attr.value;
// v-model="name" => model
var dir = attrName.substring(2);
// 事件指令
// 如果指令包含v-on则判断为事件指令
if (dir.indexOf('on') === 0) {
console.log("事件")
//compileUtil.eventHandler(node, me.$vm, exp, dir);
// 普通指令
} else {
// 在指令库中寻找对应的指令并传递节点,vm参数,和属性值进行下一步操作
compileUtil[dir] && compileUtil[dir](node, $vm, exp);
}
// 并在此节点中删除该指令
node.removeAttribute(attrName);
}
});
}
}
// 指令库
var compileUtil = {
text: function (node, vm, exp) {
//console.log(node, vm, exp)
// 交给bind来处理
this.bind(node, vm, exp, 'text');
},
bind: function (node, vm, exp, dir) {
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);
});
},
_getVMVal: function (vm, exp) {
// 这里通过遍历传进来的vm进行取值,注意如果是对象的话
// v-text = "name.a" => [name ,a],然后遍历[name, a]并从vm中获取name.a的值返回
var val = vm;
exp = exp.split('.');
exp.forEach(function (k) {
val = val[k];
});
return val;
},
}
var updater = {
textUpdater: function (node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
}
}
compile(
// el
"#demo"
// vm
, {
name: {
a: "asdas"
}
})
</script>
</body>
</html>
//数据
var obj = {
name: "winds",
age: 18
};
function observe(obj) {
//判断对象的自属性是否对象,如果时对象继续执行监听
if(!obj || typeof obj !== 'object') {
return;
}
Object.keys(obj).forEach(function(key) {
//console.log(key + '=>' + obj[key]);
//遍历监听
defineProperty(obj, key, obj[key]);
})
};
function defineProperty(obj, key, val) {
observe(val); //监听子属性,如果子属性也是对象,继续监听
Object.defineProperty(obj, key, {
set: function(newValue) {
console.log('newValue:' + newValue);
//设置新值
val = newValue;
var html = `
<p>${obj.name}</p>
<p>${obj.age}</p>
<input value="${obj.name}" />
`;
document.querySelector("#demo").innerHTML = html;
document.querySelector("input").oninput = function(e) {
console.log(e.target.value);
obj.name = e.target.value;
}
},
get: function() {
console.log("getValue:" + val);
//返回val设置对应属性值
return val;
}
})
}
observe(obj); //对对象遍历并监听每个属性值的变化
//设置值 把数组改变
obj.name = "wscats";
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="demo">
{{name.i}}
<!-- 针对 nodeType为3的情况 -->
<p>{{name}}</p><!-- 针对 nodeType为1的情况 -->
</div>
<script>
class Vue {
constructor(options = {}) {
this.$el = document.querySelector(options.el);
let data = this.data = options.data;
Object.keys(data).forEach((key) => {
this.proxyData(key);
});
this.watcherTask = {}; // 需要监听的任务列表
this.observer(data); // 初始化劫持监听所有数据
this.compile(this.$el); // 解析dom
}
// 把data挂到Vue实例化后的vm对象里面,vm.data.name获取或者改变,则触发get或者set
proxyData(key) {
let that = this;
Object.defineProperty(that, key, {
configurable: false,
enumerable: true,
get() {
console.log(that.data[key])
return that.data[key];
},
set(newVal) {
console.log(newVal)
that.data[key] = newVal;
}
});
}
observer(data) {
let that = this
Object.keys(data).forEach(key => {
let value = data[key]
this.watcherTask[key] = []
Object.defineProperty(data, key, {
configurable: false,
enumerable: true,
get() {
return value
},
set(newValue) {
if (newValue !== value) {
value = newValue
that.watcherTask[key].forEach(task => {
task.update()
})
}
}
})
})
}
compile(el) {
var nodes = el.childNodes;
console.log(nodes);
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node.nodeType === 3) {
var text = node.textContent.trim(); //移除字串前後的空白字元以及行結束字元。
console.log(text);
if (!text) continue;
this.compileText(node, 'textContent')
}
}
}
compileText(node, type) {
// 匹配出该节点下的{{}}这种写法
let reg = /\{\{(.*?)\}\}/g,
txt = node.textContent;
if (reg.test(txt)) {
node.textContent = txt.replace(reg, (matched, value) => {
// matched {{name}}
// value name
console.log(matched,value);
let tpl = this.watcherTask[value] || [];
console.log(tpl);
tpl.push(new Watcher(node, this, value, type))
if (value.split('.').length > 1) {
let v = null
value.split('.').forEach((val, i) => {
console.log(this,v);
v = !v ? this[val] : v[val]
})
return v
} else {
return this[value]
}
})
}
}
}
class Watcher {
constructor(el, vm, value, type) {
this.el = el;
this.vm = vm;
this.value = value;
this.type = type;
this.update()
}
update() {
this.el[this.type] = this.vm.data[this.value]
}
}
var data = {
name: {
i:"abc",
l:"llll"
},
skill: "ps"
};
var vm = new Vue({
el: "#demo",
data
})
console.log(vm)
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="demo">
<!-- {{name.i}} -->
<!-- 针对 nodeType为3的情况 -->
{{skill}}
<!-- 针对 nodeType为1的情况 -->
<p>{{name}}</p>
</div>
<script>
let store = {
list: {},
on: function (key, fn) {
// 如果有则继续加队列
if (!this.list[key]) {
// 没有创建新的队列
this.list[key] = [];
}
this.list[key].push(fn);
},
emit: function (key, param) {
let fns = this.list[key];
fns.forEach(fn => {
fn(param);
});
}
};
class Vue {
constructor(options = {}) {
this.$el = document.querySelector(options.el);
let data = this.data = options.data;
Object.keys(data).forEach((key) => {
this.proxyData(key);
});
this.watcherTask = {}; // 需要监听的任务列表
this.observer(data); // 初始化劫持监听所有数据
this.compile(this.$el); // 解析dom
}
// 把data挂到Vue实例化后的vm对象里面,vm.data.name获取或者改变,则触发get或者set
proxyData(key) {
let that = this;
Object.defineProperty(that, key, {
configurable: false,
enumerable: true,
get() {
return that.data[key];
},
set(newVal) {
console.log(newVal)
that.data[key] = newVal;
}
});
}
observer(data) {
let that = this
Object.keys(data).forEach(key => {
let value = data[key]
this.watcherTask[key] = []
Object.defineProperty(data, key, {
configurable: false,
enumerable: true,
get() {
return value
},
set(newValue) {
console.log(key)
if (newValue !== value) {
value = newValue
// 发布
// 更改vm的数据的时候触发set,然后会触发compileText的订阅store.on监听事件
store.emit(key, {})
}
}
})
})
}
compile(el) {
var nodes = el.childNodes;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node.nodeType === 3) {
var text = node.textContent.trim(); //移除字串前後的空白字元以及行結束字元。
if (!text) continue;
this.compileText(node, 'textContent')
}
}
}
compileText(node, type) {
var self = this;
// 匹配出该节点下的{{}}这种写法
let reg = /\{\{(.*?)\}\}/g,
txt = node.textContent;
if (reg.test(txt)) {
node.textContent = txt.replace(reg, (matched, value) => {
// matched {{name}}
// value name
// 订阅
store.on(value, () => {
//node, this, value, type
node[type] = self.data[value]
});
return this[value]
})
}
}
}
var vm = new Vue({
el: "#demo",
data: {
skill: "ps"
}
})
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="demo">
{{name}}
<p>{{name}}</p>
<input v-model="name" />
<Xheader></Xheader>
{{skill}}
</div>
<script>
class Vue {
constructor(props) {
this.$el = document.querySelector(props.el)
this.$components = props.components
// 把数据挂载到$data上面
let data = this.$data = props.data
this.watch = new Watch()
// 监听数据 数据更改,触发set的回调函数
Object.keys(data).forEach(key => {
this.proxyData(key)
})
// 监听视图 M->V 编译模板,例如解析{{}} 并设置监听器方法on
this.complie(this.$el)
}
// 把数据挂载到vm上
proxyData(key) {
let self = this;
Object.defineProperty(self, key, {
// 如果我更改name的值会触发该回调
set(newValue) {
self.$data[key] = newValue;
// 函数去更新视图
this.watch.emit(key, null)
},
// 如果我获取name的值会触发该回调
get() {
return self.$data[key]
}
})
}
complie(el) {
let nodes = el.childNodes //节点数组
// 遍历节点
for (const node of nodes) {
console.log(node)
switch (node.nodeType) {
// 处理元素节点
case 1:
if (node.hasAttribute('v-model') && (node.tagName === 'INPUT' || node.tagName ===
'TEXTAREA')) {
node.addEventListener('input', (() => {
let attrVal = node.getAttribute('v-model')
node.value = this[attrVal]
node.removeAttribute('v-model')
return () => {
this[attrVal] = node.value
}
})())
}
break;
// 处理文本节点
case 3:
this.complieText(node, 'textContent')
break;
}
}
}
// 更新视图
complieText(node, type) {
let self = this;
let reg = /\{\{(.*?)\}\}/g;
let txt = node.textContent;
if (reg.test(txt)) {
node.textContent = txt.replace(reg, (matched, value) => {
// matched {{name}}
// value name
// 订阅 this.name
this.watch.on(value, () => {
node.textContent = self.$data[value]
})
return this[value]
})
}
}
}
class Watch {
constructor() {
this.list = {}
}
on(key, fn) {
// 如果对象中没有对应的key值
// 也就是说明没有订阅过
// 那就给key创建个缓存列表
if (!this.list[key]) {
this.list[key] = []
}
// 把函数添加到对应key的缓存列表里
this.list[key].push(fn)
}
emit(key, param) {
// 根据获取改函数数组队列
let fns = this.list[key]
// 如果缓存列表里没有函数就返回false
if (!fns || fns.length === 0) {
return false;
}
// 遍历key值对应的缓存列表
// 依次执行函数的方法
fns.forEach(fn => {
// 传入参数
fn(param);
});
}
}
Vue.component = (name, options) => {
return {
name,
...options
}
}
let Xheader = Vue.component('Xheader', {
data(){
return {
name: "lemon"
}
},
template: `
<div>{{name}}</div>
`
})
var vm = new Vue({
el: "#demo",
data: {
name: "lz",
skill: "PS"
},
components: {
Xheader
}
})
console.log(vm)
</script>
</body>
</html>
<!--
观察数据 Observe Object.defineProperty M
编译模板 Compile 实现我们Vue指令的地方 {{skill}} V
观察者模式 M->V
-->