实现一个简单的vue响应式/vue响应式原理/mvvm双向绑定原理
前言
本文主要是实现一个简单版的vue.js.可以实现双向数据响应
开始
compile解析模板代码
解析模板语法,识别v-指令,给每个节点都添加上一个update更新方法,并将更新方法座位回调传递给订阅者watcher
class Compile{ constructor(vm,root){ this.$vm = vm; this.$root = root; //获取到根节点里面所有的节点通过虚拟DOM的形式操作JS对象 将{{}}等指令进行解析 //1、获取app节点下面所有的子节点 this.fragment = this.getNodeFragment(this.$root); //2、编译 this.nodeCompile(this.fragment) //3、将编译好的内容重新挂载到页面上 this.$root.appendChild(this.fragment); } getNodeFragment(root){ //在编译过程中,避免不了要操作 Dom 元素,所以这里用了一个 createDocumentFragment 方法来创建文档碎片。这在 Vue 中实际使用的是虚拟 dom,而且在更新的时候用 diff 算法来做 最小代价渲染。 //文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算) var frag = document.createDocumentFragment(); var child; //它会将root里面的每一个节点插入到内存对象中去,只要节点插入到内存对象中,该节点对应的真实dom就会在页面上消失,所以root会慢慢没,不会出现死循环的问题 while(child = root.firstChild){ frag.appendChild(child); } return frag; } nodeCompile(fragments){ //获取所有的节点 var childNodes = fragments.childNodes; Array.from(childNodes).forEach((child)=>{ if(this.isText(child)){ //编译文本节点 this.textCompile(child) } if(this.isElement(child)){ const attrs = child.attributes; //获取到节点身上的属性 Array.from(attrs).forEach((attr)=>{ const attrName = attr.name; const attrValue = attr.value; //根据属性来判断是一个指令还是一个事件 //判断是否是一个指令 if(this.dir(attrName)){ const type = attrName.substr(2); this[type+'update'] && this[type+'update'](child,this.$vm[attrValue]); new Watcher(this.$vm,attrValue,(value)=>{ this[type+'update'] && this[type+'update'](child,value); }); } //判断是否是一个事件 if(this.event(attrName)){ const type = attrName.substr(1); this.handleEvent(child,this.$vm,type,attrValue) } }) } //递归遍历查找子节点 if(child.childNodes && child.childNodes.length>0){ this.nodeCompile(child); } }) } dir(attr){ return attr.indexOf("v-") != -1; } event(attr){ return attr.indexOf("@") != -1; } isText(node){ return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent); } isElement(node){ return node.nodeType == 1; } textCompile(child){ //进行编译 this.update(child,this.$vm,RegExp.$1,'text'); } //指定v-if,v-text等会有很多,在这里做匹配具体是那个指令,然后调用该指令的方法去实现指令的操作 //这一步其实也是将每个指令对应的节点绑定更新函数 update(el,vm,exp,type){ //为了其他的一些指令需要的一些公共的逻辑在这个方法里面编写 //比如这里是text指令,updateFn=textupdate var updateFn = this[type+'update']; //如果textupdate方法存在,就去执行textupdate方法 updateFn && updateFn(el,vm[exp]); //给每个节点都添加上监听 //添加监听数据的订阅者(给watcher传递数据和回调),在index.js中一旦数据有变动会触发dep.notify方法, //nodity会遍历dep中收集的watcher,调用watcher中的update方法,在update方法中执行, //这里(compile)传递给watcher的方法(updateFn)更新视图 new Watcher(vm,exp,(value)=>{ updateFn && updateFn(el,value); }); } //v-text指令的实现操作 textupdate(el,value){ el.textContent = value; } handleEvent(el,vm,eventName,callback){ el.addEventListener(eventName,vm.$options.methods[callback].bind(vm)); } }
vue双向数据响应代码
代码中包含数据劫持,dep依赖收集,watcher订阅者实现
class Vue{ constructor(options){ this.$options = options; this.$el = document.querySelector(options.el); //接收data属性 this.$data = options.data; //将data身上所有的属性转换为响应式数据 this.observer(this.$data); new Compile(this,this.$el) } observer(data){ if(!data || typeof data != "object")return; Object.keys(data).forEach((key)=>{ //做数据劫持 this.defaultReative(data,key,data[key]); //属性代理 代理到vm身上 this.proxyData(key); }) } proxyData(key){
//将this.$data[key]代理到this即vm上 Object.defineProperty(this,key,{ get(){ return this.$data[key]; }, set(newValue){ this.$data[key] = newValue; } }) } defaultReative(data,key,value){ //先去做递归 this.observer(value); //给每个响应数据的get方法上都添加一个dep实例 var dep = new Dep(); //数据劫持 Object.defineProperty(data,key,{ get(){ /*在渲染html的时候会调用响应数据,也就执行了get方法;watcher实例是订阅者,渲染时会给当前dom初始化一个watcher实例,也就是给当前的dom添加了订阅者, * 订阅者内部会有更新dom的方法,数据变化的时候,发布者通知订阅者,订阅者内部去调用更新dom的方法. * watcher内部有dep.target赋值操作,赋值为watcher本身, 这样才能确保有我们收集的依赖就是调用了当前响应数据的订阅者,订阅者就是watcher, */ Dep.target && dep.addDep(Dep.target); return value; }, set(newVal){ if(newVal == value)return; //赋值操作 当外部设置data身上的某个属性的时候 将新值赋值给旧值 value = newVal; dep.notify(); } }) } } //依赖收集 class Dep{ constructor(){ this.deps = []; } addDep(dep){ this.deps.push(dep); } notify(){ this.deps.forEach((item)=>{ item.update(); }) } } //监听数据的变化 class Watcher{ constructor(vm,exp,callback){ this.$vm = vm; this.$exp = exp; this.callback = callback; //给dep添加了一个静态属性target,值是watcher自己 Dep.target = this; //触发属性的getter方法 this.$vm[this.$exp]; Dep.target = null; } //在index.js中一旦数据有变动会触发dep.notify方法, //nodity会遍历dep中收集的watcher,调用watcher中的update方法,在update方法中执行, //这里(compile)传递给watcher的方法(updateFn)更新视图 update(){ //视图更新 this.callback.call(this.$vm,this.$vm[this.$exp]) } }
使用
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"> <div v-text="name"></div> <button @click="handleClick">点击</button> </div> </body> </html> <script src="./index.js"></script> <script src="./compile.js"></script> <script> let vm = new Vue({ el:"#app", data:{ name:"di", }, methods:{ handleClick(){ this.name = 'didi' } } }) console.log(vm); </script>
效果
初次渲染
点击按钮
总结 vue响应数据的底层原理
vue响应数据主要是通过Object.definePrototype+发布订阅者模式实现
Observe方法用于遍历响应数据,在每个遍历代码中new Dep订阅起,给每个响应数据添加上get方法,在get方法内部添加Dep.addDep()方法,Dep.addDep()方法内部的值是watcher订阅者,这一步是依赖的收集,给每个响应数据添加上Dep.notify()方法,用于通知订阅者数据发生了改变要进行更新了
Complie方法用于解析模板,将模板中的变量替换为数据,之后渲染视图,给每个指令对应的节点添加添加更新函数,并且初始化订阅者watcher,给watcher内部传入更新函数
watcher是observe和complie之间的桥梁,在watcher方法内部会向订阅器dep中添加一个target属性指向自己,当我们去修改响应数据的时候,会触发响应数据set中的dep.notify,dep.notify就去遍历收集到的依赖,也就是会执行watcher内部的update函数.从而完成组件的更新.这就是vue响应数据的底层实现
开源中国博客地址:https://my.oschina.net/u/2998098/blog/1540520