目录html
Vue
数据独立在 data
里面,视图在 template
中dom
操做被封装
dom
来修改视图,例如 ducument.getElementById('xx').innerHTML="xxx"
Model
数据 → View
视图 → Controller
控制器MVVM
不算是一种创新ViewModel
是一种创新ViewModel
是真正结合前端应用场景的实现MVVM - Model View ViewModel
,数据,视图,视图模型Vue
的对应:view
对应 template
,vm
对应 new Vue({…})
,model
对应 data
view
能够经过事件绑定的方式影响 model
,model
能够经过数据绑定的形式影响到view
,viewModel
是把 model
和 view
连起来的链接器MVVM
框架的三大要素前端
Vue
如何监听到 data
的每一个属性变化Vue
的模板如何被解析,指令如何处理Vue
的模板如何被渲染成 html
,渲染过程是怎样的data
属性以后,Vue
马上监听到,马上渲染页面data
属性被代理到 vm
上JavaScript
对象,作属性修改,咱们监听不到,因此须要用到 Object.defineProperty
html
来显示vnode
,解决了模板中的逻辑(v-if, v-for)问题code.render
,将code打印出来,就是生成的render函数html
:vm._c
vm._c
和 snabbdom
中的 h
函数的实现很像,都是传入标签,属性,子元素做为参数Vue.js
的 vdom
实现借鉴了 snabbdom
updateComponent
中实现了 vdom
的 patch
updateComponent
data
中每次修改属性,都会执行 updateComponent
render
函数
updateComponent
,执行 vm._render()
render
函数,会访问到 data
中的值,访问时会被响应式的 get
方法监听到updateComponent
,会走到 vdom
的 patch
方法patch
将 vnode
渲染成 dom
,初次渲染完成get
,而不是直接监听 set
?
data
中有不少属性,有些被用到,有些可能不被用到get
get
中的属性,set
的时候咱们也无需关心data
属性变化,触发 re-render
set
监听到set
中执行 updateComponent
updateComponent
从新执行 vm._render()
vnode
和 prevVnode
,经过 patch
进行对比html
中index.htmlvue
这是最终的测试代码,咱们本身实现的 Vue 在 XVue.js
和 compile.js
两个文件中,加起来大概200行代码左右,主要包括功能以下:node
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title></title> </head> <body> <div id="app"> {{test}} <div v-text="test"></div> <p> <input type="text" v-model="test" /> </p> <p v-html="html"></p> <p> <button @click="onClick">按钮</button> </p> </div> <script src="./compile.js"></script> <script src="./XVue.js"></script> <script> const o = new XVue({ el: '#app', data: { test: '123', foo: { bar: 'bar' }, html: '<button>html test</button>' }, methods: { onClick() { alert('按钮点击了') } } }) console.log(o.$data.test) //123 o.$data.test = 'hello, Xvue!' console.log(o.$data.test) //hello, Xvue! </script> </body> </html>
Mini Vue 的组成部分:app
XVue.js框架
class XVue { constructor(options) { this.$data = options.data; this.observe(this.$data); // 执行编译 new Compile(options.el, this); } observe(value) { if (!value || typeof value !== 'object') { return; } Object.keys(value).forEach(key => { this.defineReactive(value, key, value[key]); // 为vue的data作属性代理 this.proxyData(key); }); } defineReactive(obj, key, val) { // 递归查找嵌套属性 this.observe(val); // 建立Dep const dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { // 收集依赖 Dep.target && dep.addDep(Dep.target); // console.log(dep.deps); return val; }, set(newVal) { if (newVal === val) { return; } val = newVal; dep.notify(); }, }); } proxyData(key) { Object.defineProperty(this, key, { get() { return this.$data[key]; }, set(newVal) { this.$data[key] = newVal; }, }); } } // 依赖管理器:负责将视图中全部依赖收集管理,包括依赖添加和通知 class Dep { constructor() { // deps里面存放的是Watcher的实例 this.deps = []; } addDep(dep) { this.deps.push(dep); } // 通知全部watcher执行更新 notify() { this.deps.forEach(dep => { dep.update(); }); } } // Watcher: 具体的更新执行者 class Watcher { constructor(vm, key, cb) { this.vm = vm; this.key = key; this.cb = cb; // 未来 new 一个监听器时,将当前 Watcher 实例附加到 Dep.target // 未来经过 Dep.target 就能拿到当时建立的 Watcher 实例 Dep.target = this; // 读取操做,主动触发 get,当前 Watcher 实例被添加到依赖管理器中 this.vm[this.key]; // 清空操做,避免没必要要的重复添加(再次触发 get 就不须要再添加 watcher 了) Dep.target = null; } update() { // console.log('from Watcher update: 视图更新啦!!!'); // 通知页面作更新 this.cb.call(this.vm, this.vm[this.key]); } }
compile.jsdom
// 扫描模板中全部依赖(指令、插值、绑定、事件等)建立更新函数和watcher class Compile { // el是宿主元素或其选择器 // vm当前Vue实例 constructor(el, vm) { this.$el = document.querySelector(el); this.$vm = vm; if (this.$el) { // 将dom节点转换为Fragment提升执行效率 this.$fragment = this.node2Fragment(this.$el); // 执行编译,编译完成之后全部的依赖已经替换成真正的值 this.compile(this.$fragment); // 将生成的结果追加至宿主元素 this.$el.appendChild(this.$fragment); } } node2Fragment(el) { // 建立一个新的Fragment const fragment = document.createDocumentFragment(); let child; // 将原生节点移动至fragment while ((child = el.firstChild)) { // appendChild 是移动操做,移动一个节点,child 就会少一个,最终结束循环 fragment.appendChild(child); } return fragment; } // 编译指定片断 compile(el) { let childNodes = el.childNodes; Array.from(childNodes).forEach(node => { // 判断node类型,作相应处理 if (this.isElementNode(node)) { // 元素节点要识别v-xx或@xx this.compileElement(node); } else if ( this.isTextNode(node) && /\{\{(.*)\}\}/.test(node.textContent) ) { // 文本节点,只关心{{msg}}格式 this.compileText(node, RegExp.$1); // RegExp.$1匹配{{}}之中的内容 } // 遍历可能存在的子节点 if (node.childNodes && node.childNodes.length) { this.compile(node); } }); } compileElement(node) { // console.log('编译元素节点'); // <div v-text="test" @click="onClick"></div> const attrs = node.attributes; Array.from(attrs).forEach(attr => { const attrName = attr.name; // 获取属性名 v-text const exp = attr.value; // 获取属性值 test if (this.isDirective(attrName)) { // 指令 const dir = attrName.substr(2); // text this[dir] && this[dir](node, this.$vm, exp); } else if (this.isEventDirective(attrName)) { // 事件 const dir = attrName.substr(1); // click this.eventHandler(node, this.$vm, exp, dir); } }); } compileText(node, exp) { // console.log('编译文本节点'); this.text(node, this.$vm, exp); } isElementNode(node) { return node.nodeType == 1; //元素节点 } isTextNode(node) { return node.nodeType == 3; //元素节点 } isDirective(attr) { return attr.indexOf('v-') == 0; } isEventDirective(dir) { return dir.indexOf('@') == 0; } // 文本更新 text(node, vm, exp) { this.update(node, vm, exp, 'text'); } // 处理html html(node, vm, exp) { this.update(node, vm, exp, 'html'); } // 双向绑定 model(node, vm, exp) { this.update(node, vm, exp, 'model'); let val = vm.exp; // 双绑还要处理视图对模型的更新 node.addEventListener('input', e => { vm[exp] = e.target.value; // 这里至关于执行了 set }); } // 更新 // 可以触发这个 update 方法的时机有两个:1-编译器初始化视图时触发;2-Watcher更新视图时触发 update(node, vm, exp, dir) { let updaterFn = this[dir + 'Updater']; updaterFn && updaterFn(node, vm[exp]); // 当即执行更新;这里的 vm[exp] 至关于执行了 get new Watcher(vm, exp, function (value) { // 每次建立 Watcher 实例,都会传入一个回调函数,使函数和 Watcher 实例之间造成一对一的挂钩关系 // 未来数据发生变化时, Watcher 就能知道它更新的时候要执行哪一个函数 updaterFn && updaterFn(node, value); }); } textUpdater(node, value) { node.textContent = value; } htmlUpdater(node, value) { node.innerHTML = value; } modelUpdater(node, value) { node.value = value; } eventHandler(node, vm, exp, dir) { let fn = vm.$options.methods && vm.$options.methods[exp]; if (dir && fn) { node.addEventListener(dir, fn.bind(vm), false); } } }