​Vue工作机制

  1. new Vue,随后进行初始化
  2. $mount挂载
  3. compile编译
  4. render函数建立虚拟DOM
  5. 依赖收集,设置监听,跟虚拟DOM匹配patch,结合defineProperty响应式更新
  6. 渲染DOM树
81787c47e2f37df28095cf6b2ff430d6.png

本文精简模拟代码:

      
姓名:{{name}}
年龄:{{age}}

页面展示:

072a8a340f3890195d9aac03804dfab1.png

1s后:

e1553418415ee048198f497c0660092b.png

不能这样操作

Object.keys(_data).forEach(key => {    Object.defineProperty(_data, key, {        get() {            return _data[key];        },        set(newVal) {            if (newVal === _data[key]) return;            // render();        }    });});

否则会报错

3f748c6b30ca630e1934c11629738e05.png

模拟代码解析

1.初始化数据

let el=document.getElementById('div1');let template=el.innerHTML;​render();​function render(){  el.innerHTML=template.replace(/{{w+}}/g, str=>{  str=str.substring(2, str.length-2);  return _data[str];});

2.compile编译、render函数

let el=document.getElementById('div1');let template=el.innerHTML;​render();​function render(){  el.innerHTML=template.replace(/{{w+}}/g, str=>{  str=str.substring(2, str.length-2);  return _data[str];});
  • template.replace(/{{w+}}/g,识别指令语法{{}}
  • str.substring(2, str.length-2),获取变量名name、age
  • _data[str],返回data对应key的值
  • el.innerHTML渲染DOM

Vue中compile编译的核心逻辑:

获取dom,遍历dom,获取{{}}、v-和@开头的 ,设置响应式

24f05aa8b5960df84d753f1425d59231.png

上图简化为三步:(打开冰箱,放入大象,关上冰箱)

this.$el = document.querySelector(el)if (this.$el) {  // 第一步:获取dom  this.$fragment = this.node2Fragment(this.$el)  // 第二步:编译dom  this.compileElement(this.$fragment)  // 第三步:回填dom  this.$el.appendChild(this.$fragment)}

获取dom

this.$el = document.querySelector(el)// 使用文档碎片fragment来移动比较高效率this.$fragment = this.node2Fragment(this.$el)​node2Fragment(el) {    // 新建文档碎片 dom接口   let fragment = document.createDocumentFragment()   let child   // 将原生节点拷贝到fragment   while (child = el.firstChild) {    fragment.appendChild(child)  }   return fragment }

编译dom

this.compileElement(this.$fragment)​ compileElement(el) {   let childNodes = el.childNodes   Array.from(childNodes).forEach((node) => {    let text = node.textContent    // 表达式文本    // 就是识别{{}}中的数据    let reg = /{{(.*)}}/    // 按元素节点方式编译    if (this.isElementNode(node)) {     this.compile(node)   } else if (this.isTextNode(node) && reg.test(text)) {      // 文本 并且有{{}}     this.compileText(node, RegExp.$1)​   }    // 遍历编译子节点(递归)    if (node.childNodes && node.childNodes.length) {     this.compileElement(node)   }  }) }​compile(node) {   let nodeAttrs = node.attributes   Array.from(nodeAttrs).forEach( (attr)=>{    // 规定:指令以 v-xxx 命名    // 如  中指令为 v-text    let attrName = attr.name // v-text    let exp = attr.value // content    if (this.isDirective(attrName)) {      let dir = attrName.substring(2) // text      // 普通指令      this[dir] && this[dir](node, this.$vm, exp)   }    if(this.isEventDirective(attrName)){      let dir = attrName.substring(1) // text      this.eventHandler(node, this.$vm, exp, dir)   }  }) }​compileText(node, exp) {   this.text(node, this.$vm, exp)      3.Vue响应式原理核心defineProperty

3.Vue响应式原理核心defineProperty

数据的read、write在js底层是依靠defineProperty属性来实现,我们的做法是改写它的方法来实现数据的劫持。

Object.keys(_data).forEach(key => {    defineReactive (_data, key, _data[key]);});​​function defineReactive(obj, key, val) {    Object.defineProperty(obj, key, {        get() {            return val;        },        set(newVal) {            if (newVal === val) return;            val = `拦截set:${newVal}`        }    });}

上图代码实现中,定时器演示1s后改变name值,页面响应式刷新

setTimeout(() => {      _data.age = 24;      render()  }, 1000)

有点眼熟,defineProperty是JS原生属性,平时不起眼,在大神手里都是幕后主角的存在。

Vue中对响应式做了进一步处理:依赖收集与追踪

正是通过在defineProperty的 get 属性触发时添加收集函数追踪实现目的

新建一个 Dep 类的对象,用来收集 Watcher 对象。读数据的时候,会触发defineProperty中 get 函数把当前的Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中去。

写数据的时候,则会触发defineProperty中 set 方法,通知Dep 类调用 notify 来触发所有 watcher 对象的update 方法更新对应视图

d685e2fcf6820eb766137f944a881588.png

简化后的依赖收集类

// 依赖收集class Dep {  constructor () {    // 存数所有的依赖    this.deps = [] }  // 在deps中添加一个监听器对象  addDep (dep) {      this.deps.push(dep)   }  depend() {    Dep.target.addDep(this)  }  // 通知所有监听器去更新视图  notify () {    this.deps.forEach((dep) => {      dep.update()   }) }}

简化后的监听器类

// 监听器class Watcher {   constructor(vm, key, cb) {    // 在new一个监听器对象时将该对象赋值给Dep.target,在get中会用到    // 将 Dep.target 指向自己    // 然后触发属性的 getter 添加监听    // 最后将 Dep.target 置为空    this.cb = cb    this.vm = vm    this.key = key    this.value = this.get()  }   get() {      Dep.target = this      let value = this.vm[this.key]      return value    }   // 更新视图的方法   update() {       this.value = this.get()       this.cb.call(this.vm, this.value)    }}

一个变量可以在多处使用:所以一个变量可能对应多个Watcher 对象

总结:

1.vue的编译过程是怎样的

什么是编译,为什么要编译:因为使用vue编写的模板语句,html根本不识别,我们通过编译的过程可以进行依赖收集,进行依赖收集以后我们就将data中的数据模型跟视图之间产生了绑定关系(依赖关系),以后如果模型发生变化的时候我们就可以通知这些依赖的地方让它们进行更新,这就是我们执行编译的目的。我们把页面全部编译以后,进行更新操作就可以做到模型驱动视图的变化,这就是vue编译的的过程,这就是它的作用。

2.双向绑定的原理是什么

我们在做双向数据绑定的时候通常会放一个v-model在input元素上,为什么要放v-model?因为我们编译的时候可以解析出v-model,在操作的时候:把当前v-model所属的元素上加了一个事件监听,把v-model指定的事件的回调函数作为input事件监听的回调函数去监听,这样的话,如果input发生变化的时候我们就可以把最新的值设置到vue的实例上,因为vue实例已经实现了数据的响应化,响应化的setter函数会触发界面中所有模型的依赖的更新,会通知所有的依赖进行更新刷新的操作,所以在界面中跟这个数据相关的所有部分就更新了。

Logo

前往低代码交流专区

更多推荐