ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
>[success] # 手写一个简单vue数据响应 ~~~ 1.实现一个简单的vue 响应的模型,刚才已经分析过了整体思路,现在实现一个简单的 vue 版本,分析一下Vue 在使用时候一些规则。 1.1.首先接受的是一个对象,其中data可以是对象也可以是function 1.2.data 属性可以直接通过vue实例调用 2.对下面几个类做个说明: 2.1.Vue 类一个配置入口,其中'_proxyData'方法将data中的数据挂载到vue实例上 2.2.Observe 类负责对data 中每个属性都进行getter 和setter,并且对数据变化进行监听调用 2.3.解析指令和差值表达式 ~~~ * vue 官网的图解 ~~~ 1.发布者Dep 在数据getter时候,将需要的观察者 收集到发布者中,在setter进行发送通知 1.watcher 作为一个观察者,等待setter 时候触发整体调用机制 ~~~ ![](https://img.kancloud.cn/81/e7/81e765156986df52c7374621fefdddf6_725x465.png) >[danger] ##### 最后整体效果 * 使用效果 ~~~ <div id="app"> <div> <p>{{name}}</p> <p>{{name}}</p> <p v-text="sex"></p> </div> </div> <!--下面的js 被放到一个test.js 文件中--> <script src="./test.js"></script> <script> const vm = new Vue({ el: '#app', data: { name: 'w', sex: 'female', } }) console.log(vm) </script> ~~~ ~~~ function getData(data, vm) { try { return data.call(vm, vm) } catch (e) { handleError(e, vm, "data()"); return {} } } class Vue { constructor(options) { // 1.将options 进行保存 this.$opitions = options || {} const data = options.data this.$data = typeof data === 'function' ? getData(data, vm) : data || {} this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el // 2.将data 中数据转换成getter setter 存在vue 中, // 这次只是将 data 中的数据 以getter setter 形式放到Vue 对象中 this._proxyData(this.$data) // 3.调用observe对象,observe 对象负责对data 中每个属性都进行getter 和setter // 并且对数据变化进行监听调用 new Observe(this.$data) // 4. 调用compiler对象,解析指令和差值表达式 new Compiler(this) } _proxyData(data) { // 遍历data中的所有key为了将数据形成getter 和setter 的形式 Object.keys(data).forEach(key => { Object.defineProperty(this, key, { enumerable: true, configurable: true, get() { return data[key] }, set(newValue) { // 如果新老值没有变化就不用进行set 重新赋值 if (newValue !== data[key]) { data[key] = newValue } } }) }) } } class Observe { constructor(data) { this.walk(data) } // 循环遍历data对象的所有属性,作为递归的一个判断条件 walk(data) { if (typeof data !== 'object' || !data) return Object.keys(data).forEach(key => { this.defineReactive(data, key, data[key]) }) } defineReactive(data, key, val) { let that = this // 负责收集依赖,并发送通知 let dep = new Dep() console.log(dep, 111) this.walk(val) Object.defineProperty(data, key, { enumerable: true, configurable: true, get() { // 将需要的观察者 收集到发布者中 Dep.target && dep.addSub(Dep.target) return val }, set(newValue) { if (newValue === val) return val = newValue // 如果赋值的 是对象需要从新进行getter 和setter 绑定 that.walk(newValue) // 发送通知 dep.notify() } }) } } // 处理判el 对象绑定是 function query(el) { if (typeof el === 'string') { var selected = document.querySelector(el); if (!selected) { warn( 'Cannot find element: ' + el ); return document.createElement('div') } return selected } else { return el } } class Compiler { constructor(vm) { this.el = query(vm.$el) this.vm = vm // 调用处理模板 this.compile(this.el) } // 编译模板 // 处理文本节点和元素节点 compile(el) { // 获取节点中的内容 const childNodes = el.childNodes // 需要对不同节点做不同的处理 // 1.1.文本节点一般是通过{{msg}},双大括号这种进行值替换 // 1.2.dom 节点一般会自定义一些指令 v-if 或者 v-text 一类 Array.from(childNodes).forEach(node => { if (this.isTextNode(node)) { this.compileText(node) } else if (this.isElementNode(node)) { this.compileElement(node) } // 如果当前节点还有子节点 这时候就需要递归继续compile操作 if (node.childNodes && node.childNodes.length) { this.compile(node) } }) } // 判断是不是 文本节点,这是dom 自己的属性3 表示文本 isTextNode(node) { return node.nodeType === 3 } // 判断是不是属性节点,这是dom 自己的属性1 表示dom 元素 isElementNode(node) { return node.nodeType === 1 } // 判断是不是指令 也就是v- 开头 isDirective(attrName) { return attrName.startsWith('v-') } // 编译文本节点 compileText(node) { // 1.文本节点一般是通过{{msg}},双大括号这种进行值替换 // 现在的思路就是利用正则取出 双大括号中的key 并且在vm找到key 对应的value渲染上 const reg = /\{\{(.+)\}\}/ // 获取文本节点中的内容例如<p>我是{{ name }}</p> // 获取的内容就为 -- 我是{{ name }} const value = node.textContent if (reg.test(value)) { // 获取双大括号中的key const key = RegExp.$1.trim() // 重新赋值 文本节点中的内容 // 之前已经将data 中的数据通过_proxyData方法放到vue 实例上了 node.textContent = value.replace(reg, this.vm[key]) // 创建watcher对象,当数据改变更新视图 new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } } // 编译属性节点 compileElement(node) { // 遍历dom 节点所有元素中的属性,找到指令 Array.from(node.attributes).forEach(attr => { // 获取元素中的属性 let attrName = attr.name if (this.isDirective(attrName)) { // attrName 的形式 v-text v-model // 截取属性的名称,获取 text model attrName = attrName.substr(2) // 获取属性的名称,属性的名称就是我们数据对象的属性 v-text="name",获取的是name const key = attr.value // 处理不同的指令 this.update(node, key, attrName) } }) } // 负责更新 DOM // 创建 Watcher update(node, key, dir) { // node 节点,key 数据的属性名称,dir 指令的后半部分 const updateFn = this[dir + 'Updater'] updateFn && updateFn.call(this, node, this.vm[key], key) } // v-text 指令的更新方法 textUpdater(node, value, key) { node.textContent = value new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } // v-model 指令的更新方法 modelUpdater(node, value, key) { node.value = value new Watcher(this.vm, key, (newValue) => { node.value = newValue }) // 双向绑定 node.addEventListener('input', () => { this.vm[key] = node.value }) } } // -------利用发布订阅 模式来进行 数据响应通知---------- // 发布者 -- 发布者要收集所有观察者才能 给每个观察者发送要接受的内容 class Dep { constructor() { this.subs = [] } addSub(sub) { if (sub && sub.update) { this.subs.push(sub) } } notify() { this.subs.forEach(sub => sub.update()) } } // 观察者 等待发布者发送指令 class Watcher { constructor(vm, key, cb) { this.vm = vm // data 中的key this.key = key // 执行的回调 this.cb = cb // 把watcher对象记录到Dep类的静态属性target Dep.target = this // 触发get方法,在get方法中会调用addSub this.oldValue = vm[key] Dep.target = null } // 当数据发生变化的时候更新视图 update() { let newValue = this.vm[this.key] if (this.oldValue === newValue) { return } this.cb(newValue) } } ~~~ >[danger] ##### 参考文章 [mvvm](https://github.com/DMQ/mvvm)