vue - 原理
vue 原理
大概思路
vue的数据驱动主要实现建立在三个对象上Dep、Watcher、Compiler
Dep 主要负责依赖的收集
Watcher 主要负责Dep和Compiler之间的联系
Compiler 可以理解为 virtual dom + patch 也就是负责视图层的渲染
基本原理
1、建立虚拟DOM Tree,通过document.createDocumentFragment(),遍历指定根节点内部节点,根据{{ prop }}、v-model等规则进行compile;
2、通过Object.defineProperty()进行数据变化拦截;
3、截取到的数据变化,通过发布者-订阅者模式,触发Watcher,从而改变虚拟DOM中的具体数据;
4、通过改变虚拟DOM元素值,从而改变最后渲染dom树的值,完成双向绑定
完成数据的双向绑定在于Object.defineProperty()
双向绑定的实现
简易双绑
原理 Object.defineProperty
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
语法:Object.defineProperty(obj, prop, descriptor)
var obj = {};
Object.defineProperty(obj,'hello',{
get:function(){
//我们在这里拦截到了数据
console.log("get方法被调用");
},
set:function(newValue){
//改变数据的值,拦截下来额
console.log("set方法被调用");
}
});
obj.hello//输出为“get方法被调用”,输出了值。
obj.hello = 'new Hello';//输出为set方法被调用,修改了新值
通过以上方法可以看出,获取对象属性值触发get、设置对象属性值触发set,因此我们可以想象到数据模型对象的属性设置和读取可以驱动view层的数据变化,view的数据变化传递给数据模型对象,在set里面可以做很多事情。
数据的双向绑定
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据的双向绑定</title>
</head>
<body>
<input class="inp-text" type="text">
<div class="text-box"></div>
<button class="btn">getInputValue</button>
<script>
var obj = {
name: 'init'
};
console.log(JSON.stringify(obj)) // {"name":"init"}
Object.defineProperty(obj, 'name', {
set: function (newValue) {
console.log('触发setter:',newValue);
document.querySelector('.text-box').innerHTML = newValue;
document.querySelector('.inp-text').value = newValue;
},
get: function () {
console.log('触发getter');
}
});
console.log(JSON.stringify(obj)) // {}
document.querySelector('.inp-text').addEventListener('keyup', function (e) {
obj.name = e.target.value;
}, false);
document.querySelector('.btn').addEventListener('click', (function (e) {
// ???: // console.log(obj.name) // undefined
console.log(obj)
}), false);
</script>
</body>
</html>
虚拟DOM树
创建虚拟DOM
var frag = document.createDocumentFragment();
view层的{{msg}}和v-model的编译规则如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>虚拟DOM树</title>
</head>
<body>
<!-- view层做了多层嵌套,这样测试更多出现错误的可能性。 -->
<div id="container">
{{ msg }}<br>
<input class="inp-text" type="text" v-model="inpText">
<div class="text-box">
<p class="show-text">{{ msg }}</p>
</div>
</div>
<script>
var container = document.getElementById('container');
//这里我们把vue实例中的data提取出来,更加直观
var data = {
msg: 'Hello world!',
inpText: 'Input text'
};
var fragment = virtualDom(container, data);
container.appendChild(fragment);
//虚拟dom创建方法
function virtualDom(node, data) {
let frag = document.createDocumentFragment();
let child;
console.log(node)
// 遍历dom节点
while (child = node.firstChild) {
compile(child, data);
frag.appendChild(child);
}
console.log(frag)
return frag;
}
//编译规则
function compile(node, data) {
let reg = /\{\{(.*)\}\}/g;
if (node.nodeType === 1) { // 标签
let attr = node.attributes;
for (let i = 0, len = attr.length; i < len; i++) {
// console.log(attr[i].nodeName, attr[i].nodeValue);
if (attr[i].nodeName === 'v-model') {
let name = attr[i].nodeValue;
node.value = data[name];
}
}
if (node.hasChildNodes()) {
node.childNodes.forEach((item) => {
compile(item, data); // 递归
});
}
}
if (node.nodeType === 3) { // 文本节点
if (reg.test(node.nodeValue)) {
let name = RegExp.$1;
name = name.trim();
node.nodeValue = data[name];
}
}
}
</script>
</body>
</html>
解释:
1、通过virtualDom创建虚拟节点,将目标盒子内所有子节点添加到其内部,注意这里只是子节点;
2、子节点通过compile进行编译,a:如果节点为元素,其nodeType = 1,b:如果节点为文本,其nodeType = 3,具体可以查看详情;
3、如果第二步子节点仍有子节点,通过hasChildNodes()来确认,如果有递归调用compile方法。
else
浏览器运行时会把template转换为render函数,webpack则不需要,因为vue-loader已经转换完成
get数据收集是在render函数中执行的,每个computed函数都会生成一个watcher和data里的数据绑定,data变化后watcher执行,(所以才有了缓存),每个组件都会生成一个渲染watcher,里面主要是做微任务部分,总之同步任务做数据收集,微任务做diff及dom渲染。