自己实现 一个 Vue框架,包含了Vue的核心原理
Vue工作机制
- vue工作机制
- Vue响应式的原理
- 依赖收集与追踪
- 编译compile
html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> </head> <body> <div id="app"> <!-- 插值绑定 --> <p>{{name}}</p> <!-- 指令系统 --> <p k-text="name"></p> <p>{{age}}</p> <p>{{doubleAge}}</p> <!-- 双向绑定 --> <input type="text" k-model="name"> <!-- 事件处理 --> <button @click="changeName">呵呵</button> <!-- html内容解析 --> <div k-html="html"></div> </div> <script src='./compile.js'></script> <script src='./kvue.js'></script> <script> new KVue({ el: '#app', data: { name: "I am test.", age: 17, html: '<button>这是一个按钮</button>' }, created() { console.log('开始啦'); setTimeout(() => { this.name = '我是异步事件' }, 1500) }, methods: { changeName() { this.name = '哈喽,哈哈哈哈'; this.age = 20; this.id = 'xx' ; console.log(1, this) ; } } }) </script> </body> </html>
kvue.js
/* * @Author: liguowei01 * @Date: 2019-12-31 11:17:12 * @Last Modified by: liguowei01 * @Last Modified time: 2020-01-02 17:55:53 */ // 用法: new KVue({data:{...}}) class KVue { constructor(options) { this.$options = options; //数据的响应化 this.$data = options.data; this.observe(this.$data); //观察数据 //模拟一下watcher创建 // new Watcher(); //实例一 // this.$data.test; // new Watcher(); //实例二 实例二不等于实例一 // this.$data.foo.bar; new Compile(options.el, this); //生命周期函数 //created if (options.created) { //options.created(); //本来是这样执行,下面的调用call()方法,为函数指定执行作用域 options.created.call(this); //这样就可以在created函数中用this了。 } } observe(obj) { //检验数据类型必须是对象 if (!obj || typeof obj !== 'object') { return; } //遍历该对象 Object.keys(obj).forEach(key => { this.defineReactive(obj, key, obj[key]); //代理配置项 data 中的属性到vue实例上 this.proxyData(key); }) } //数据响应化(数据劫持) defineReactive(obj, key, val) { this.observe(val); //递归解决数据的嵌套 const dep = new Dep(); Object.defineProperty(obj, key, { get() { Dep.target && dep.addDep(Dep.target); return val }, set(newVal) { if (newVal === val) { return; } val = newVal; // console.log(`${key}属性更新了:${newVal}`) dep.notify(); } }) } //代理函数() proxyData(key) { Object.defineProperty(this, key, { get() { return this.$data[key]; }, set(newVal) { this.$data[key] = newVal; } }) } } //vue 数据绑定的原理是什么? //首先,把vue选项里的data中的每个属性都利用了Object.defineProperty()定义了一个属性, //都定义了get和set这样的话让我们的机会监听数据和变化, //当这些属性发生变化时,我们可以通知那些需要更新的地方去更新 //依赖搜集 //Dep: 用来管理 Watcher class Dep { constructor() { //这里存在若干依赖(watcher,一个watcher对应一个属性) this.deps = []; } //添加依赖的方法,搜集依赖时,往这里面放东西 addDep(dep) { this.deps.push(dep) } //通知方法,用来通知所有的watcher 去更新 notify() { this.deps.forEach(dep => dep.updata()) } } //Watcher 用来做具体更新的对象 class Watcher { constructor(vm, key, cb) { this.vm = vm; this.key = key; this.cb = cb; //将当前watcher实例指定到Dep静态属性target Dep.target = this; this.vm[this.key]; //触发getter,添加依赖 Dep.target = null; } updata() { // console.log('属性更新了'); this.cb.call(this.vm, this.vm[this.key]) } }
Compile.js
/* * @Author: liguowei01 * @Date: 2020-01-02 10:34:50 * @Last Modified by: liguowei01 * @Last Modified time: 2020-01-03 09:18:12 */ //用法 new Compile(el,vm) class Compile { constructor(el, vm) { //要遍历的宿主节点 this.$el = document.querySelector(el); this.$vm = vm; //在其他方法中方便使用 //编译 if (this.$el) { //转换内部内容为片段Fragment this.$fragment = this.node2Fragment(this.$el); //执行编译 this.compile(this.$fragment); //将编译完的html追加到$el this.$el.appendChild(this.$fragment); } } //将宿主元素中的代码片段拿出来遍历,这样做比较高效 node2Fragment(el) { //创建一个代码块 const frag = document.createDocumentFragment(); //将el中所有子元素“搬家”(移动)到frag中 let child; while (child = el.firstChild) { frag.appendChild(child); } return frag; } compile(el) { const childNodes = el.childNodes; Array.from(childNodes).forEach(node => { //判断类型 if (this.isElement(node)) { //元素 console.log('编译元素', node.nodeName); //查找k-, @, : const nodeAttrs = node.attributes; Array.from(nodeAttrs).forEach(attr => { const attrName = attr.name; const exp = attr.value; if (this.isDirective(attrName)) { //k-text const dir = attrName.substring(2); //执行指令 this[dir] && this[dir](node, this.$vm, exp); } if (this.isEvent(attrName)) { //@click let dir = attrName.substring(1); // text this.eventHandler(node, this.$vm, exp, dir); } }) } else if (this.isInterpolation(node)) { //插值文本{{}} console.log('编译文本', node.nodeName); this.compileText(node); } //递归子节点 if (node.childNodes && node.childNodes.length > 0) { this.compile(node) } }) } isDirective(attr) { return attr.indexOf('k-') == 0; } isEvent(attr) { return attr.indexOf('@') == 0; } isElement(node) { return node.nodeType === 1; } //插值文本 isInterpolation(node) { return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent); } //编译文本 compileText(node) { //console.log(RegExp.$1); //正则对象RegExp的静态属性$1就是第一个匹配的值 就是上面'name' //node.textContent = this.$vm.$data[RegExp.$1]; this.updata(node, this.$vm, RegExp.$1, 'text'); } /* * @作用: 更新函数 根据指令决定是哪个更新器 它将来需要知道(参数) * @params: node 更新的节点 * @params: vm kvue的实例 * @params: exp 正则表达式 匹配的结果 如:name * @params: dir 指令(文本、事件、其他) 如:text,html,model * 这个方法是个通用方法,将来要被调用很多次 */ updata(node, vm, exp, dir) { const updaterFn = this[dir + 'Updater']; //在当前的类里面组合一个函数名 /*这种写法和 this.a 一样,this是代表当前对象,也是一个对象, 对象名.方法名 或 对象名.属性名 调用对象中的属性和方法 还有一种调用方式:对象名['方法名'] 或 对象名['属性名'] 也可以使用 对象名['方法名']() 执行此方法 */ //先判断updaterFn是否存在,如果存在则执行 updaterFn && updaterFn(node, vm[exp]); //初始化(第一次) //依赖收集 new Watcher(vm, exp, function(value) { //观察vm 里的exp(属性),并在属性变化时,如何更新 updaterFn && updaterFn(node, value); }) } //更新的具体操作 textUpdater(node, value) { node.textContent = value; } text(node, vm, exp) { this.updata(node, vm, exp, 'text'); } // 事件处理 eventHandler(node, vm, exp, dir) { let fn = vm.$options.methods && vm.$options.methods[exp]; if (dir && fn) { node.addEventListener(dir, fn.bind(vm), false); } } model(node, vm, exp) { this.updata(node, vm, exp, 'model'); let val = vm.exp; node.addEventListener('input', (e) => { let newValue = e.target.value; vm[exp] = newValue; val = newValue; }) } modelUpdater(node, value) { node.value = value; } html(node, vm, exp) { this.updata(node, vm, exp, 'html'); } htmlUpdater(node, value) { node.innerHTML = value; } }
/*
问题1:vue编译过程是怎样的?
遵循3W1H原则,什么是编译,为什么要编译。
首先写的这些模板的语句,html根本就不能识别,
我们通过编译的过程,可以进行依赖的收集,
进行依赖收集以后,我们就把data中的数据模型和视图之间产生了绑定关系
产生了依赖关系,那么以后模型发生变化的时候,
我们就会通知这些依赖的地方让他们进行更新,
这就是我们执行编译的目的,这样就做到了模型驱动视图的变化。
问题2:双向绑定的原理是什么?
做双向绑定时,通常在表单元素上绑定一个v-model,
我们在编译的时候,可以解析到v-model
操作时做了两件事:
1.在表单元素上做了事件监听(监听input、change事件)
2.如果值发生变化时,在事件回调函数把最新的值设置到vue的实例上
3.因为vue的实例已经实现了数据的响应化,
它的响应化的set函数会触发,通知界面中所有模型的依赖的更新。
所以界面中的,跟这个数据相关的部分就更新了
*/