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渲染。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步