class Vue { constructor(options) { this.$options = options this.$el = document.body this.$data = options.data() this.$methods = options.methods this.$mounted = options.mounted // 数据劫持 this.observe(this.$data) // 编译数据 this.compile(this.$el) } // 遍历劫持数据 observe(obj) { if (typeof obj !== 'object') { return; } Object.keys(obj).forEach(key => { // 递归遍历所有层次 this.observe(obj[key]) // 创建观察者 new Oberver(obj, key) // 数据代理: this.$data.title ---> this.title this.proxyData(key) }) } // 数据代理 proxyData(key) { Object.defineProperty(this, key, { get() { return this.$data[key] }, set(v) { this.$data[key] = v } }) } // 编译 compile(el) { new Compile(this, el) } $mount(sel) { this.$el = document.querySelector(sel) const update = () => { if (!this.mounted) { // 首次执行, 实现挂载 this.mounted = true if (this.$mounted) { this.$mounted() } } } update() } }
// 观察者 class Oberver { constructor(obj, key) { this.defineReactive(obj, key, obj[key]) } // 定义响应式: 遍历data中的每个数据,都要生成一个Dep容器,这个Dep容器用来收集该数据产生的依赖(即:每使用一次该数据,就会产生一个Watcher,用来update更新) defineReactive(obj, key, val) { const dep = new Dep() // 遍历data中的每一个数据,并生成相应的Dep容器 Object.defineProperty(obj, key, { get() { // 每一次访问数据 this.title , 都会往Dep容器的实例里面deps推入一个watcher if (Dep.target) { // 每次使用该数据的时候,都要创建一个依赖(即Watcher,方便未来进行数据更新) dep.addDep(Dep.target) } return val }, set(v) { if (val !== v) { // 数据值改变时,给该数据赋新值,并通知(notity)该数据的所有依赖进行更新 val = v // 数据重新赋值,[通知]该数据所有的依赖更新数据 dep.notice() } } }) } }
// 依赖收集容器Dep:即,管理wathcer的管理者。data中的每一个数据,都要生成一个dep容器,用来收集 // Dep容器,data中的每个数据会对应一个,用来收集并存储依赖(依赖: 就是 template中的 插值表达式,v-,@等等的数据) // Dep对象有一个静态属性target,用来存放Watcher实例---即依赖deps数组中的元素---即Dep.target class Dep { constructor() { // 每一项数据的依赖收集在这个数组中, 每一个依赖,就是一个Watcher this.deps = [] } addDep(dep) { this.deps.push(dep) } notice() { this.deps.forEach(dep => { // 这里的dep是一个watcher dep.update() }) } }
// Watcher: 编译时{{}},v-等,每访问一次数据,就要创建一个watcher实例 // 三个参数,vm:vue实例,方便是用 // 什么时候进行wathcer实例化呢? // 在编译的时候,每次遇到{{}},v-,@ 等时,就要创建一个实例 class Watcher { constructor(vm, key, callback) { this.$cb = callback // 回调函数用来更新数据 Dep.target = this // 将Watcher实例存储一个全局变量中,存到Dep.target中,方便get方法,收集依赖 vm[key] // 触发get方法,在get方法中收集依赖(defineReactive中定义了) Dep.target = null // Dep.target置空,方便下一次使用数据时,存储Watcher实例化时 } update() { // 执行回调函数,来更新数据 this.$cb() } }
知识储备
1. nodeType: 1:元素节点 3:文本节点
2. fragment节点: 存在内存中的文档片段,并不在DOM树中--> 将子元素插入fragment文档片段中,不会引起页面回流(对元素位置和几何上的计算)。所以,更好的性能
3. const reg = /\{\{.*\}\}/
可以匹配{{name}}。
但要提取出name,还需要在 .*
外加一层()
。这一对小括号就是一个捕获组,可以帮助我们在匹配字符串的同时并捕获字符串中更精细的信息。
RegExp.$1
是RegExp的一个属性,指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串
以此类推,RegExp.$2
,RegExp.$3
,……RegExp.$99
总共可以有99个匹配
// 编译: 1. document-> fragment 2. fragment中将 {{}}、v-、@等 提取出并进行相应操作 3. 将fragment转为dom class Compile { constructor(vm, el) { this.$vm = vm this.$el = el if (this.$el && this.isElementNode(this.$el)) { this.$fragment = this.node2Fragment(this.$el) this.compileFragment(this.$fragment) this.$el.appendChild(this.$fragment) } } // 将节点转化为fragment文档片段,在内存中操作不直接操作dom,不会引起页面回流 node2Fragment(el) { const fragment = document.createDocumentFragment() let child while (child = el.firstChild) { // el.firstChil:返回文档的首个子节点,将el.firstChild赋值给child,并且当child===undefined就跳出循环 fragment.appendChild(child) // appendChild会把原来的firstChild给移动到新的文档中, el中firstChild随之就会递进一个元素。 } return fragment } // 编译fragment,提取出{{}}/v-/@,并创建相应的wathcer(依赖) compileFragment(fragment) { const nodes = fragment.childNodes // 伪数组 Array.from(nodes).forEach(node => { if (this.isInterpolation(node)) { // 是插值表达式,提取出变量 this.compileText(node) } else if (this.isElementNode(node)) {// 是v-model、v-html、v-text、@change this.compileElement(node) } node.childNodes.length > 0 && this.compileFragment(node) }) } compileText(node) { const key = this.getInterKey(node) this.text(node, key) } compileElement(node) { const attrs = node.attributes // {0: {name:'class', value: 'active'}, length: 1} Array.from(attrs).forEach(attr => { if (attr.name.startsWith('v-')) { // v-model = "inputValue" this[attr.name.substring(2)](node, attr.value) // model、html/text } else if (attr.name.startsWith('@')) { this.eventHandler(node, attr.name.substring(1), attr.value) } }) } // 事件处理 eventHandler(node, eventName, methodName) { node.addEventListener(eventName, (e) => { this.$vm.$methods[methodName].call(this.$vm, e.target.value, e) }) } // 通过data数据的key值,更新数据,需要用到watcher text(node, key) { new Watcher(this.$vm, key, () => { node.textContent = this.$vm.$data[key] }) node.textContent = this.$vm.$data[key] } // 通过data数据的key值,更新数据,需要用到watcher html(node, key) { new Watcher(this.$vm, key, () => { node.innerHtml = this.$vm[key] }) node.innerHtml = this.$vm[key] } // 通过data数据的key值,更新数据,需要用到watcher model(node, key) { new Watcher(this.$vm, key, () => { node.value = this.$vm[key] }) node.value = this.$vm[key] node.addEventListener('input', (e) => { this.$vm[key] = e.target.value }) } // 提取出插值表达式中的变量:data数据对应的key {{title}} getInterKey(node) { const reg = /\{\{(.*)\}\}/ // 正则一对小括号就是一个捕获组,可以帮助我们在匹配字符串的同事捕获字符串中更精细的信息 node.textContent.match(reg) return RegExp.$1.trim() } // 判断是否是差值表达式 {{ title }}: 1. 文本节点; 2. 有花括号 isInterpolation(node) { return node.nodeType === 3 && /\{\{.*\}\}/.test(node.textContent) } // 判断节点是否为元素节点 isElementNode(el) { return el && el.nodeType === 1 } }