<!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"> <p>{{ name }}</p> <div v-text="name"></div> <input v-model="name" type="text"> </div> <script> // 手写一个mvvm 简易版的vuejs // options就是选项 全部vue属性都带$ function Vue (options) { this.subs = {} // 事件管理器 this.$options = options // 放置选项 this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el // 将dom对象赋值给$el 和官方vuejs保持一致 this.$data = options.data || {} // 数据代理 但愿 vm可以代理 $data的数据 // 但愿 vm.name 就是$data.name this.$proxyData() // 代理数据 把$data中数据 代理给vm实例 this.$observer() // 数据劫持 劫持 $data中的数据变化 this.$compile(this.$el) // 模板第一次编译渲染 递归的要求 这里必须传入参数 // 递归是一种简单的算法 => 通常用在处理树形数据,嵌套数据 中国/北京/海淀/中关村/知春路/海淀桥/982/人 // 递归其实就是函数自身调用自身 => 传入下一次递归的条件 => 两次递归条件同样 => 死循环了 } // 数据代理好的方法 Vue.prototype.$proxyData = function () { // this 指的就是 当前的实例 // key 就是 data数据中的每个key Object.keys(this.$data).forEach(key => { // 代理数据 让vm实例代理$data中的数据 Object.defineProperty(this, key, { // 存取描述符 get () { return this.$data[key] // 返回$data中的数据 }, // 设置数据时 须要 将 值设置给 $data的值 并且要判断设置以前数据是否相等 set (value) { // value是新值 若是新值等于旧值 就不必再设置了 if (this.$data[key] === value ) return this.$data[key] = value // 若是不等再设置值 } }) }) } // 数据劫持 Vue.prototype.$observer = function () { // 要劫持谁 ? $data // 遍历 $data中的全部key Object.keys(this.$data).forEach(key => { // 劫持 =>劫持数据的变化 -> 监听 data中的数据的变化 => set方法 // obj / prop / desciptor let value = this.$data[key] // 从新开辟一个空间 value的空间 Object.defineProperty(this.$data, key, { // 描述 => 描述符有几种 ? 数据描述符(value,writable) 存取描述符 (get/set) get () { return value }, set: (newValue) => { if(newValue === value) return value = newValue // 一旦进入set方法 表示 MVVM中的 M 发生了变化 data变化了 // MVVVM => Model => 发布订阅模式 => 更新Dom视图 // 总体编译只执行一次 经过发布订阅模式来作 触发一个事件 视图层监听一个事件 // 触发一个事件 this.$emit(key) // 把属性当成事件名 触发一个事件 } }) }) } // 编译模板 数据发生变化 => 模板数据更新到最新 // 编译模板的一个总方法 构造函数执行时执行 // rootnode是传入本次循环的根节点 => 找rootnode下全部的子节点 => 子节点 => 子节点=> 子节点 > 子节点 ... 找到没有子节点为止 Vue.prototype.$compile = function (rootnode) { let nodes = Array.from(rootnode.childNodes) // 是一个伪数组 将伪数组转成真数组 nodes.forEach(node => { // 循环每一个节点 判断节点类型 若是你是文本节点 就要用文本节点的处理方式 若是元素节点就要元素节点的处理方式 if(this.$isTextNode(node)) { // 若是是文本节点 this.$compileTextNode(node) // 处理文本节点 当前的node再也不有 子节点 没有必要继续找了 } if(this.$isElementNode(node)) { // 若是是元素节点 this.$compileElementNode(node) // 处理元素节点 // 若是是元素节点 下面必定还有子节点 只有文本节点才是终点 // 递归了 => 自身调用自身 this.$compile(node) // 传参数 保证一层一层找下去 找到 node.chidNodes的长度为0的时候 自动中止 // 能够保证 把 $el下的全部节点都遍历一遍 } }) } // 处理文本节点 nodeType =3 Vue.prototype.$compileTextNode = function (node) { // console.log(node.textContent) // 拿到文本节点内容以后 要作什么事情 {{ name }} => 真实的值 // 正则表达式 const text = node.textContent // 拿到文本节点的内容 要看一看 有没有插值表达式 const reg = /\{\{(.+?)\}\}/g // 将匹配全部的 {{ 未知内容 }} if (reg.test(text)) { // 若是能匹配 说明 此时这个文本里有插值表达式 // 表示 上一个匹配的正则表达式的值 const key = RegExp.$1.trim() // name属性 => 取name的值 $1取的是第一个的key node.textContent = text.replace(reg, this[key] ) // 获取属性的值 而且替换 文本节点中的插值表达式 this.$on(key, () => { // 若是 key这个属性所表明的值发生了变化 回调函数里更新视图 node.textContent = text.replace(reg, this[key] ) // 把原来的带大括号的内容替换成最新值 赋值给textContent }) } } // 处理元素节点 nodeType = 1的时候是元素节点 Vue.prototype.$compileElementNode = function (node) { // 指令 v-text v-model => 数据变化 => 视图更新 更新数据变化 // v-text = '值' => innerText上 textContent // 拿到该node全部的属性 let attrs = Array.from(node.attributes) // 把全部的属性转化成数组 // 循环每一个属性 属性是否带 v- 若是带 v- 表示指令 attrs.forEach(attr => { if (this.$isDirective( attr.name)) { // 判断指令类型 if(attr.name === 'v-text') { // v-text的指令的含义是 v-text后面的表达的值 做用在 元素的innerText或者textContent上 node.textContent = this[attr.value] // 赋值 attr.value => v-text="name" this.$on(attr.value, () => { node.textContent = this[attr.value] //此时数据已经更新 }) } if(attr.name === 'v-model') { // 表示我要对当前节点进行双向绑定 node.value = this[attr.value] // v-model要给value赋值 并非textContent this.$on(attr.value, () => { node.value = this[attr.value] //此时数据已经更新 }) node.oninput = () => { // 须要把当前最新的节点的值 赋值给 自己的数据 this[attr.value] = node.value // 视图 发生 => 数据发生变化 } // 若是一个元素绑定了v-model指令 应该监听这个元素的值改变事件 } } // 若是以 v-开头表示 就是指令 }) } // 判断一个节点是不是文本节点 nodeType ===3 Vue.prototype.$isTextNode = function (node) { return node.nodeType === 3 // 表示就是文本节点 } // 判断 一个节点是不是元素节点 Vue.prototype.$isElementNode = function (node) { return node.nodeType === 1 // 表示就是元素节点 } // 判断一个属性是不是指令 全部的指令都以 v-为开头 Vue.prototype.$isDirective = function (attrname) { return attrname.startsWith('v-') } // Vue的发布订阅管理器 $on $emit // 监听事件 Vue.prototype.$on = function (eventName, fn) { // 事件名 => 回调函数 => 触发某个事件的时候 找到这个事件对应的回调函数 而且执行 // if(this.subs[eventName]) { // this.subs[eventName].push(fn) // }else { // this.subs[eventName] = [fn] // } this.subs[eventName] = this.subs[eventName] || [] this.subs[eventName].push(fn) } // 触发事件 Vue.prototype.$emit = function (eventName, ...params) { // 拿到了事件名 应该去咱们的开辟的空间里面 找有没有回调函数 if(this.subs[eventName]) { // 有人监听你的事件 // 调用别人的回调函数 this.subs[eventName].forEach(fn => { // 改变this指向 // fn(...params) // 调用该回调函数 而且传递参数 // 三种方式 改变回调函数里的this指向 // fn.apply(this, [...params]) // apply 参数 [参数列表] // fn.call(this, ...params) // 若干参数 fn.bind(this, ...params)() // bind用法 bind并不会执行函数 而是直接将函数this改变 }); } } var vm = new Vue({ el: '#app', // 还有多是其余选择器 还有多是dom对象 data: { name: '吕布', wife: '貂蝉' } }) </script> </body> </html>