# data-binding **Repository Path**: denghuoan/data-binding ## Basic Information - **Project Name**: data-binding - **Description**: 模拟vue双向绑定 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-04-29 - **Last Updated**: 2021-11-02 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # [vue响应式](https://cn.vuejs.org/v2/guide/reactivity.html) ## 思路 基于数据劫持双向绑定的实现思路: 1. **数据劫持Observer**: 利用`Proxy`或`Object.defineProperty`生成的Observer针对对象/对象的属性进行"劫持",在属性发生变化后通知订阅者 2. **解析Compile**: 解析器Compile解析模板中的`Directive`(指令),收集指令所依赖的方法和数据,等待数据变化然后进行渲染 3. **检测Watcher**: Watcher属于Observer和Compile桥梁,它将接收到的Observer产生的数据变化,并根据Compile提供的指令进行视图渲染,使得数据变化促使视图变化 ## 几个概念 ### 1.数据劫持 #### [defineProperty (Vue 2.x)](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty) 语法: `Object.defineProperty(obj, prop, descriptor)` - obj: 要定义属性的对象 - prop: 要定义或修改的属性的名称或symbol - descriptor: 要定义或修改的属性描述符 ```html
Hello
``` - 多个属性时 ```html
Hello
``` #### [proxy (Vue 3.x)](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy) 语法:`const p = new Proxy(target, handler)` ```html
Hello
``` ### 2. 发布&订阅模式与观察者模式 - **观察者模式**: 是由具体目标调度,比如当事件触发,Dep就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖度的 - **发布/订阅模式**: 由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在 ![image-20210415155858082](./images/img-04.png) #### 发布&订阅模式 > 我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(**publish**)一个信号,其他任务可以向信号中心"订阅"(**subscribe**)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern) > > eg: 中国移动下有查询流量余额与话费余额两个业务,使用流量时扣话费,欠费时流量禁用, ##### vue自定义事件 ##### 模拟vue自定义事件 ```js class EventEmitter { constructor() { this.subs = {} // { give: [handler1, handler2], 'flow-event': ?, 'bill-event': ?} } // 触发 $emit(eventType) { this.subs[eventType].forEach(handler => { handler() }) } // 注册 $on(eventType, handler) { this.subs[eventType] = this.subs[eventType] || [] this.subs[eventType].push(handler) } } const sub = new EventEmitter() // 组件1 sub.$on('give', () => { console.log('object1'); }) // 组件2 sub.$on('give', () => { console.log('object2'); }) sub.$on('bill-event', () => { console.log('bill-event'); }) // 发布 sub.$emit('give') sub.$emit('bill-event') ``` #### 观察者模式 - 举例 ```html Document
{{msg}}
{{msg}}
``` - 依赖收集 > 当你的text3变化的时候,实际上text3并没有被渲染,但是也会触发一次render函数,这显然是不对的。所以我们需要收集依赖。 > 我们只需要在初始化的时候渲染一遍,那所有渲染所依赖的数据都会被触发getter,这时候我们只要把这个数据放到一个列表里就好啦! ```js new Vue({ template: `
text1: {{text1}} text2: {{text2}}
`, data: { text1: 'text1', text2: 'text2', text3: 'text3' } }); ``` - 封装 ```html
{{msg}}
{{msg}}
{{number}}
``` ## Vue响应式模拟 **整体分析** ![img-01](./images/img-01.png) - Vue 把 data 中的成员注入到 Vue 实例,并且把 data 中的成员转成 getter/setter - Observer 能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知 Dep - Compiler 解析每个元素中的指令/插值表达式,并替换成相应的数据 - Dep 添加观察者(watcher),当数据变化通知所有观察者 - Watcher 数据变化更新视图 ### Vue - 负责接收初始化的参数(选项) - 负责把 data 中的属性注入到 Vue 实例,转换成 getter/setter - 负责调用 Observer 监听 data 中所有属性的变化 - 负责调用 Compiler 解析指令/插值表达式 ### Observe - 负责把 data 选项中的属性转换成响应式数据 - data 中的某个属性也是对象,把该属性转换成响应式数据 - 情况1: ```js data: { msg: 'hello', person: { name: 'cc' } } ``` - 情况2: ```js data.msg = { name: 'aa' } ``` - 数据变化发送通知 ### Compiler - 负责编译模板,解析指令/插值表达式 - nodeType: ` 1元素节点 2属性节点 3文本节点 8注释节点` 等 - 负责页面的首次渲染 ```js class Compiler { constructor(vm) { this.vm = vm this.el = vm.$el this.compile(this.el) } compile (el) { const childNodes = el.childNodes Array.from(childNodes).forEach(node => { if (this.isTextNode(node)) this.compileText(node) if (this.isElementNode(node)) this.compileElement(node) // 判断node节点,是否有子节点,如果有子节点,要递归调用compile if (node.childNodes && node.childNodes.length) this.compile(node) }) } // 处理文本节点 - 差值表达式 - {{ msg }} compileText (node) { // console.log(node, 15) const reg = /\{\{(.+?)\}\}/ // 源码中: /\{\{((?:.|\r?\n)+?)\}\}/g const value = node.textContent if (reg.test(value)) { const key = RegExp.$1.trim() node.textContent = value.replace(reg, this.vm[key]) } } // 处理元素节点 - 'v-' compileElement (node) { // console.log(node, 18) // 遍历属性节点 Array.from(node.attributes).forEach(attr => { console.dir(attr.name) let attrName = attr.name if (this.isDirective(attrName)) { // v-text v-model const key = attr.value attrName = attrName.slice(2) this.update(attrName, key, node) } }) } update (attrName, key, node) { const updateFn = this[attrName + 'Update'] updateFn && updateFn.call(this, node, key) } textUpdate (node, key) { // v-text="msg" node.textContent = this.vm[key] } modelUpdate (node, key) { // v-model="msg" node.value = this.vm[key] } // 判断元素属性是否是指令 isDirective (attrName) { return attrName.startsWith('v-') } // 判断节点是否是文本节点 isTextNode (node) { return node.nodeType === 3 } // 判断节点是否是元素节点 isElementNode (node) { return node.nodeType === 1 } } ``` - 当数据变化后重新渲染视图 ### Dep ![image-20210414150312052](./images/img-02.png) - 在compiler中收集依赖,添加观察者(watcher) - 通知所有观察者 ```js // defineReactive中: 创建dep对象收集依赖 const dep = new Dep() // getter中: get的过程中收集依赖 Dep.target && dep.addSub(Dep.target) // setter中: 当数据变化之后,发送通知 dep.notify() ``` ### Watcher ![image-20210414150952714](./images/img-03.png) - 当数据变化触发依赖,dep通知所有的Watcher实例更新视图 - 自身实例化的时候往dep对象中添加自己 ```js class Watcher { constructor(vm, key, cb) { this.vm = vm this.key = key this.cb = cb // 在Dep的静态属性上记录当前watcher对象,当访问数据时把watcher添加到dep的subs中 Dep.target = this // 触发一次getter, 让dep为当前key记录Watcher this.oldValue = vm[key] // 清空target Dep.target = null } update () { // 负责更新视图 console.log(this.name, ':我去更新DOM元素了') const newValue = this.vm[this.key] if (this.oldValue === newValue) return this.cb(newValue) } } ``` ### 总结 - Vue: - 记录传入的选项,设置$data/$el - 把data的成员注入到Vue实例 - 负责调用Observe实现数据响应式处理(数据劫持) - 负责调用Compiler编译指令/插值表达式等 - Observe - 数据劫持 - 负责把data中的成员转换成getter/setter - 负责把多层属性转换成getter/setter - 如果给属性赋值为新对象,把新对象的成员设置为getter/setter - 添加Dep和Watcher的依赖关系 - 数据变化发送通知 - Compiler - 负责编译模板,解析指令/插值表达式 - 负责页面的首次渲染过程 - 当数据变化后重新渲染 - Dep - 收集依赖,添加订阅者(watcher) - 通知所有订阅者 - Watcher - 自身实例化的时候往dep对象中添加自己 - 当数据变化dep通知所有的Watcher实例更新视图