知识要点
- vue工作机制
- vue响应式原理
- 依赖收集与追踪
- 编译compile
Vue工作机制
在new Vue() 之后。Vue会调用进行初始化,会初始化生命周期、事件、props、methods、data、computed 与 watch 等。其中最重要的是通过 Object.defineProperty 设置 setter 与 getter,用来实现 响应式 以及 依赖收集
初始化之后调用 $mount 挂载组件
简化版:
编译
编译模块分为三个阶段
1、parse:使用正则解析template中的vue指令(v-xxx)变量等等,形成抽象语法树AST
2、optimize:标记一些静态节点,用作后面的性能优化,在diff的时候直接略过
3、generate:把第一步生成的AST转化为渲染函数 render function
响应式
这一块是vue最核心的内容,初始化的时候通过defineProperty定义对象getter、setter,设置通知机制
当编译生成的渲染函数被实际渲染的时候,会触发getter进行依赖收集,在数据变化的时候,触发setter进行更新
虚拟dom
Virtual DOM 是react首创,Vue2开始支持,就是用JavaScript对象来描述dom结构,数据修改的时候,我们先修改虚拟dom中的数据,然后数组做diff,最后再汇总所有的diff,力求做最少的dom操作,毕竟js里对比很快,而真实的dom操作太慢
更新视图
数据修改触发setter,然后监听器会通知进行修改,通过对比新旧vdom树,得到最小修改,就是patch,然后只需要把这些差异修改即可
Vue2响应式原理:Object.defineProperty
实现 KVue:
// 定义KVue的构造函数 class KVue { constructor(options) { // 保存选项 this.$options = options // 传入data this.$data = options.data // 响应化处理 this.observe(this.$data) // 测试代码 // new Watcher(this, 'foo') // this.foo // new Watcher(this, 'foo.bar.mua') // this.bar.mua // 测试编译器 new Compile(options.el, this) if(options.created) { options.created.call(this) } } observe(value) { // 如果value不存在,或者value不是对象,就直接return // 这里不对数组进行处理 if (!value || Object.prototype.toString.call(value) !== '[object Object]') { return } // 遍历value Object.keys(value).forEach(key => { // 响应式处理 this.defineReactive(value, key, value[key]) // 代理data中的属性到vue根上 this.proxyData(key) }) } defineReactive(obj, key, val) { // 这里实质上就是一个闭包,内部的get和set一直使用的是这里的val this.observe(val) // 如果是对象的话,进一步递归 // 定义了一个Dep const dep = new Dep() // 每个dep实例和data中每个key有一对一关系 // 给obj的每一个key定义拦截 Object.defineProperty(obj, key, { get() { // 依赖收集 Dep.target && dep.addDep(Dep.target) return val }, set(newVal) { if (newVal !== val) { val = newVal // console.log(key + '属性更新了') dep.notify() // 通知 } } }) } // 在vue根上定义属性代理data中的数据 proxyData(key) { // this指的是KVue的实例 Object.defineProperty(this, key, { get() { return this.$data[key] }, set(newVal) { this.$data[key] = newVal } }) } } // 创建Dep:管理所有的Watcher(观察订阅模式) class Dep { constructor() { // 存储所有依赖 this.watchers = [] } addDep(watcher) { this.watchers.push(watcher) } notify() { this.watchers.forEach(watcher => watcher.update()) } } // 创建Watcher:保存data中数值和页面中的挂钩关系 class Watcher { constructor(vm, key, cb) { // 某个组件中的某个key // 创建实例时立刻将该实例指向Dep.target,便于依赖收集 Dep.target = this this.vm = vm this.key = key this.cb = cb Dep.target = this this.vm[this.key] // 读一下,触发依赖收集 Dep.target = null } // 更新 update() { // console.log(this.key + '更新了!!!') this.cb.call(this.vm, this.vm[this.key]) } }
编译compile
核心任务:
1、获取并遍历DOM树
2、文本节点:获取 {{}} 格式的内容并解析
3、元素节点:访问节点特性,截获 k- 和 @开头内容并解析
// 遍历dom结构,解析指令和插值表达式 class Compile { // el:待编译模板 vm:KVue实例 constructor(el, vm) { this.$vm = vm this.$el = document.querySelector(el) // 把模板中的内容移到片段中操作 this.$fragment = this.node2Fragment(this.$el) // 执行编译 this.compile(this.$fragment) // 放回$el中 this.$el.appendChild(this.$fragment) } node2Fragment(el) { // 创建片段:游离于dom文档之外,做修改的话不会让文档刷新 const fragment = document.createDocumentFragment() let child while (child = el.firstChild) { fragment.appendChild(child) } return fragment } compile(el) { const childNodes = el.childNodes Array.from(childNodes).forEach(node => { if (node.nodeType === 1) { // 元素 // console.log('编译元素' + node.nodeName) this.compileElement(node) } else if (this.isInter(node)) { // 判断是否是文本节点 // 只关心{{xxx}}的文本节点 // console.log('编译插值文本' + node.textContent) this.compileText(node) } // 递归子节点 if (node.children && node.childNodes.length > 0) { this.compile(node) } }) } isInter(node) { return node.nodeType === 3 && /{{(.*)}}/.test(node.textContent) } // 文本替换 compileText(node) { // console.log(RegExp.$1) // 表达式 const exp = RegExp.$1 this.update(node, exp, 'text') // v-text // node.textContent = this.$vm[RegExp.$1] } update(node, exp, dir) { const updator = this[dir + 'Updator'] updator && updator(node, this.$vm[exp]) // 创建Watcher实例,依赖收集完成了 new Watcher(this.$vm, exp, function (value) { updator && updator(node, value) }) } textUpdator(node, value) { node.textContent = value } compileElement(node) { // 关心属性 const nodeAttrs = node.attributes Array.from(nodeAttrs).forEach(attr => { // 规定:k-xxx="yyy" const attrName = attr.name // k-xxx const exp = attr.value // yyy if (attrName.indexOf('k-') === 0) { // 指令 const dir = attrName.substring(2) // xxx // 执行 this[dir] && this[dir](node, exp) } }) } text(node, exp){ this.update(node, exp, 'text') } }
测试代码:
<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> <div k-html="html"></div> </div> <script src='./compile.js'></script> <script src="./kvue.js"></script> <script> const app = new KVue({ el: '#app', data: { name: "I am test.", age: 12, html: '<button>这是⼀一个按钮</button>' }, created() { console.log('开始啦') setTimeout(() => { this.name = '我是测试' console.log('结束啦') }, 1500) }, methods: { changeName() { this.name = '哈喽,开课吧' this.age = 1 } } }) </script> </body>
v-html指令
html(node, vm, exp) { this.update(node, vm, exp, 'html') } htmlUpdator(node, value) { node.innerHTML = value }
v-model指令
model(node, vm, exp) { this.update(node, vm, exp, 'model') node.addEventListener('input', e => { vm[exp] = e.target.value }) } modelUpdator(node, value) { node.value = value }
事件监听
compileElement(node) { // 关心属性 const nodeAttrs = node.attributes Array.from(nodeAttrs).forEach(attr => { // 规定:k-xxx="yyy" const attrName = attr.name // k-xxx const exp = attr.value // yyy if (attrName.indexOf('k-') === 0) { // 指令 const dir = attrName.substring(2) // xxx // 执行 this[dir] && this[dir](node, exp) } // 事件处理 if(attrName.indexOf('@') === 0) { const dir = attrName.substring(1) // 事件名称 // 事件监听处理 this.eventHandler(node, this.$vm, exp, dir) } }) } // 事件处理:给node添加事件监听,dir-事件名称 // 通过vm.$options.methods[exp]可获得回调函数 eventHandler(node, vm, exp, dir) { let fn = vm.$options.methods && vm.$options.methods[exp] if (dir && fn) { node.addEventListener(dir, fn.bind(vm)) } }