Skip to content

Latest commit

 

History

History
2159 lines (1664 loc) · 64.5 KB

File metadata and controls

2159 lines (1664 loc) · 64.5 KB

从零手写Vue2部分

实际搭建

使用Rollup搭建开发环境

npm init -y

进行一个初始化

npm install rollup  rollup-plugin-babel @babel/core @babel/preset-env --save-dev

安装rollup,babel,然后让其在保存在本地,并且只在调试中运行

创建rollup.config.js,以便通过rollup进行打包。

将package.json里面的script配置项中的test配置项,名字换为dev,内容变为rollup -cw -c指定配置文件,-w监视文件变化image-20230917152923260

创建src入口文件夹,里面创建index.js作为入口文件

配置rollup.config.js文件

import babel from 'rollup-plugin-babel'
//rollup默认可以导出一个对象 作为打包的配置文件
export default{
  input:'./src/index.js',//入口
  output:{
    file:'./dist/vue.js',//出口
    format:'umd',//打包模式  常见有esm  es6模块  commonjs模块    iife自执行函数  umd统一模块规范(commonjs && amd)
    name:'Vue',// global.Vue
    sourcemap:true,//希望可以调试代码
  },
  plugins:[
    babel({
      exclude: 'node_modules/**' //排除node_modules所有的文件
    })
  ],//插件
}

配置babel文件 .babelrc 预设引入插件

{
  "presets":[
    "@babel/preset-env"
  ]
}

初始化数据

Vue没有使用class进行创建,而是使用的构造函数,通过构造函数来扩建方法,这样扩建的方法可以放在不同文件中,而类必须放在一个类声明中,非常臃肿

index.js

//将所有的方法耦合在一起
import {initMixin} from './init'

function Vue(options){//options就是用户的选项
  this._init(options);//初始化操作
}

initMixin(Vue);//扩展了init方法 

export default Vue

init.js

import { initState } from "./state";

export function initMixin(Vue){//给Vue 增加init方法
  Vue.prototype._init = function(options){//用于初始化操作
    //vue vm.$options 就是获取用户的配置 
    const vm = this; //
    vm.$options = options;//把用户配置赋值给vm.,挂载到vm身上 使用$标识表示这是vue里面的
    
    //初始化状态
    initState(vm);
  }
}

像这样,然后不同添加的方法可以放在某某文件中,然后进行导入添加上就OK。

比较多的方法可以放在其他文件里面

比如需要initState函数

state.js

import { observe } from "./observe/index";
export function initState(vm){
  const opts = vm.$options;//获取所有的选项
  if(opts.data){//如果有data选项那么初始化data
    initData(vm);
  }
}

function initData(vm){
  let data = vm.$options.data;//data可能是函数,可能是对象
  // debugger;
  data = typeof data === 'function' ?  data.call(vm) : data;//call进行执行函数\
  vm._data = data;//将data挂载到vm._data
  //对数据进行劫持,Vue2使用defineProperty
  observe(data);
}

实现对象响应式原理

对对象进行深度劫持

Vue2使用的api defineProperty

/observe/index.js

class Observer{
  constructor(data){
    //Object.defineProperty只能劫持已经存在的
    this.walk(data);
  }
  walk(data){//循环对象   对属性依次劫持
    //  "重新定义"属性
    Object.keys(data).forEach(key => defineReactive(data, key, data[key]));
  }
}

export function defineReactive(data, key, val){//形成闭包,值不会消失
      observe(val);//这里是递归处理对象,如果data的属性中存在对象,那么就会继续递归下去处理,因此Vue2的数据劫持性能较低,但是数组还没有处理
  Object.defineProperty(data,key,{
    get(){//取值会执行get
      return val;
    },
    set(newVal){
      if(newVal !== val){
        val = newVal;
        console.log('数据被修改了');
      }
    }
  })
}
export function observe(data){
  if(typeof data !== 'object' || data === null) return;//只对对象劫持
  //如果一个对象被劫持了,那么就不需要再被劫持了(判断一个对象是否被劫持过,可以添加一个实例,用实例进行判定)
  return new Observer(data);
}

像上面完成了代理,但是比较阴间,因为每次需要data中的xxx属性都需要vm._data.xxx

因此可以将vm._data进行代理,放在vm上,这是state.js补充的内容

function proxy(vm,target,key){
    Object.defineProperty(vm,key,{
        get(){
            return vm[target][key];
        },
        set(newVal){
            vm[target][key] = newVal;
        }
    })
}


//.....省略了一些东西
function initData(vm){
    //...
    observe(data);
    
    for(let key in data){//将vm._data用vm进行代理
        proxy(vm,'_data',key);
    }
}

或者写成另一种情况

function initData(vm){
    //...
    observe(data);
    proxy(vm,'_data');
}

function proxy(vm,target){
    Object.keys(target).forEach(key =>{
        Object.defineProperty(vm,key,{
            get(){
                return vm[target][key];
            },
            set(newVal){
                vm[target][key] = newVal
            }
        })
    })
}

实现数组的数据劫持

数组如果仍然是用对象的方式去添加,虽然能添加上,但是改动一个,又要全部重新更新,非常不方便。

修改数组很少使用索引进行直接操作,因为类似于 a[9999] = 0,内部进行劫持非常消耗性能。

而且大多数时候是通过shift,unshift,push,pop这些操作进行操作的

不可枚举enumerabel:false,是指不可循环、不可以取值。

observe/index.js

class Observe{
    constructor(data){
      //数组单独处理
      data.__ob__ = this;//这里可以给data添加一个__ob__属性,这个属性指向Observer实例,这样就可以通过__ob__属性访问Observer实例了
      Object.defineProperty(data, '__ob__', {
        value:this,
        enumerable:false//将ob变为不可枚举否则会陷入死循环,因为进入data后枚举到其__ob__属性(实际回到了本身),那么就会出现问题
      })
      // 同时给数据加了标识,数据上有ob那么对象被代理过,但是也需要其变成不可枚举属性
      if(Array.isArray(data)){
          //这里我们可以重写数组中的七个编译方法,可以修改数组本身的。除此之外还有数组内的引用方法也应该劫持,比如一个对象作为数组的内容
          //同时保留其他的方法,因此需要在array.js里面重写
          data.__proto__ = newArrayProto;
          this.observeArray(data);//观测数组
      }else{
          this.walk(data);
      }   
  }
    walk(data){
      Object.keys(data).forEach(key => defineReactive(data,key,data[key]))
  }
    observeArray(){
        data.forEach(item => observe(item))
    }
    //剩下的和之前一样
}
//.....
export function observe(data){
  if(typeof data !== 'object' || data === null) return;//只对对象劫持
  //如果一个对象被劫持了,那么就不需要再被劫持了(判断一个对象是否被劫持过,可以添加一个实例,用实例进行判定)

  if(data.__ob__ instanceof Observer){//说明被代理过 就不需要再次代理
    return data.__ob__;
  }
  return new Observer(data);
}

observe/array.js

//重写数组的部分方法

 let oldArrayProto = Array.prototype;//获取数组的原型
export let newArrayProto = Object.create(oldArrayProto);//将原有的原型加在新数组原型上

//找到所有的变异方法

let methods =[
  'push','pop','shift','unshift','splice','sort','reverse'
]
methods.forEach((method)=>{
  //arr.push(1,2,3)
  newArrayProto[method] = function(...args){
    //调用原来的方法修改数组,函数的劫持,切片编程
   const result =  oldArrayProto[method].call(this, ...args);
    // console.log('method',method);
    //对新增的数据再次进行劫持
    let inserted;
    let ob = this.__ob__;
    switch(method){
      case 'push':
      case 'unshift':
        inserted = args;
        break;
      case 'splice'://arr.splice(0,1,{a:1})//第一个是删除的位置,第二个是个数
        inserted = args.slice(2);
        break;
      default:
        break;
    }
    if(inserted){
      //对新增内容再次进行观测,这是新的数组,可以使用observeArray进行观测,那么可以将Observer的实例对象,放在data的某一个值上
      ob.observeArray(inserted);
    }
   return result;
  }
})

解析模版参数

比如插值语法等等

模版引擎 性能比较差需要正则匹配 1.0的时候,没有引入虚拟DOM的改变

2.采用虚拟DOM,数据变化后比较虚拟DOM的差异 最后渲染到页面

3.核心:我们需要将模版变成我们的js语法,通过js语法生成虚拟DOM

从一个东西变成一个东西,语法之间的转化,es6=>es5

css压缩 我们需要先变成语法树再重新组装代码成为新的语法 将template语法换成render函数

渲染优先顺序,render函数>template>el

需要安装新插件

npm install @rollup/plugin-node-resolve

这样可以导入的时候自动找寻index文件,就只用写import xxx from './compiler'实际上是import xxx from './compiler/index.js'

转译

比如使用正则去匹配,正则的规则如下(Vue3不是使用正则)

const startTagOpen = new RegExp(`^<${qnameCapture}`);//它匹配到的分组是一个标签名<xxx> 匹配开始标签名
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);//匹配的是</xxx> 最终匹配到结束标签名
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;//匹配属性 a="xxxx" b  =  'xxx' c = xxx
//第一个分组是属性的key,value是分组3或分组4或者分组5
const startTagClose = /^\s*(\/?)>/;   //可以匹配<div>或者<br/>
const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g;//匹配双括号,内容是表达式的变量

首先在init里面进行修改,因为初始化完毕,那么就需要解析模版进行渲染了

init.js

import { initState } from "./state";
import { compileToFunction } from "./compiler/index";
export function initMixin(Vue){//给Vue 增加init方法
  Vue.prototype._init = function(options){//用于初始化操作
    //vue vm.$options 就是获取用户的配置 
    const vm = this; //
    vm.$options = options;//把用户配置赋值给vm.,挂载到vm身上 使用$标识表示这是vue里面的
    //初始化状态
    initState(vm);
    //判断是否由el属性
    if(options.el){
      vm.$mount(options.el);//挂载 
    }
  }
  Vue.prototype.$mount = function(el){
    const vm = this;
    el = document.querySelector(el);
    let op = vm.$options;
    if(!op.render){//如果没有render函数
      let template;//没有render看是否写了template。没有写template采用外部的template
      if(!op.template && el){//没有模版但是有el
         template = el.outerHTML;
      }else{
          template = op.template;//如果有el则采用模版
      }
      // console.log(template);
      if(template){//存在模版就对模版进行编译
        const render = compileToFunction(template);
        op.render = render;//把编译后的render函数赋值给render
      }
    }
    op.render;
    //script标签引用的vue.global.js这个编译过程是在浏览器进行的,runtime是不包含模版编译的,整个编译是打包的时候通过loader来转义.vue文件的
    //用runtime的时候不能使用template模版的
  }
}

转化为ast(抽象语法树),可以使用栈结构。 遇到开始标签扔进去标签名,遇到结束标签弹出,这样就可以逐步得到,该标签的父结构和子结构分别是什么

主结构compiler/index.js

import { parseHTML } from "./parse";
export function compileToFunction(template){
  //1.将template转化为ast语法树
  let ast = parseHTML(template);
  // 2.生成render函数(render方法执行后返回的结果是虚拟DOM)
}

compiler/parse.js将得到的字符串转化为html结构,DOM树

const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;

const startTagOpen = new RegExp(`^<${qnameCapture}`);//它匹配到的分组是一个标签名<xxx> 匹配开始标签名
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);//匹配的是</xxx> 最终匹配到结束标签名 分组1是标签名
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;//匹配属性 a="xxxx" b  =  'xxx' c = xxx
//第一个分组是属性的key,value是分组3或分组4或者分组5
const startTagClose = /^\s*(\/?)>/;   //可以匹配<div>或者<br/>
const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g;//匹配双括号,内容是表达式的变量

export function parseHTML(html){// html最开始肯定是一个<
  //最终需要转化为一颗抽象的语法树

  const  ELEMENT_TYPE = 1;//元素类型1
  const TEXT_TYPE = 3;//文本类型3
  const stack = [];//存放标签的栈
  let currentParent = null;//指向栈中的最后一个
  let root = null;//根节点

  function createASTElement(tag,attrs){
    return {
      tag: tag,
      type: ELEMENT_TYPE,
      children: [],
      attrs,
      parent: null
    }
  }


  function start(tag,attrs){//给标签名和属性
    const node = createASTElement(tag,attrs);//创造ast结点
    if(!root){
      root = node;//如果树为空,则为根节点
    }
    if(currentParent){
      node.parent = currentParent;//当前结点的父节点是当前的父节点
      currentParent.children.push(node);//把它父亲结点的儿子指向它;

    }
    stack.push(node);//把当前的标签名压入栈中
    currentParent = node;
  } 
  function chars(text){//文本放在当前结点中
    text = text.replace(/\s/g,'');//如果空格超过两个以上就删除两个以上
    text &&currentParent.children.push({
      type: TEXT_TYPE,
      text,
      parent: currentParent
    })
  }
  function end(tag){
    stack.pop();//弹出最后一个
    currentParent = stack[stack.length - 1];
  }
  function advance(len){
    html = html.substring(len);
  }
  function parseStartTag(){
    const start = html.match(startTagOpen);//
    if(start){
      const match = {
        tagName: start[1],
        attrs: [],
        // start: index
      }
      advance(start[0].length);//匹配上了就进行截取
      // console.log(match,html);
      let attr,end;
      while(!(end = html.match(startTagClose)) &&(attr = html.match(attribute))){
        advance(attr[0].length);
        match.attrs.push({
          name: attr[1],
          value: attr[3] || attr[4] || attr[5] || true
        }) //true是为了弄disabled
      }
      if(end)advance(end[0].length);//结束的尖角号
      return match;
    }
    //如果不是开始标签的结束就一直匹配
    
    // console.log(html);
    return false;
  }
  while(html){
    //如果textEnd为0,说明是一个开始标签或者结束标签   如果textEnd>0说明就是文本的结束位置
    let textEnd = html.indexOf('<');//如果indexOf中的索引是0,则说明是个标签
    if(textEnd === 0){
      const startTagMatch = parseStartTag();//开始标签的匹配结果 
      if(startTagMatch){//解析到开始标签
        start(startTagMatch.tagName,startTagMatch.attrs);
        continue;
      }
      let endTagMatch = html.match(endTag);//匹配结束标签
      if(endTagMatch){
        advance(endTagMatch[0].length);
        end(endTagMatch[1]);
        // console.log(endTagMatch);
        continue;
      }
    }
    if(textEnd > 0){
      let text = html.substring(0,textEnd);//文本内容
      if(text){
        chars(text);//将文本内容传递给chars
        advance(text.length);
      }
    }
  }
  // console.log(root);
  return root;
}

代码生成

compiler/index.js

这里面就是生成字符串以便后面的生成,_c是标签,_v是文本,_s是插值语法

import { parseHTML } from "./parse";

function genProps(attrs){
  let str = '';
  for(let i=0;i<attrs.length;i++){
    let attr = attrs[i];
    if(attr.name === 'style'){
      //  color:red ==> {color:'red'}
      let obj = {};
      attr.value.split(';').forEach(item=>{//qs库
        let [key,value] = item.split(':');
        obj[key] = value;
      });
      attr.value = obj;
    }
    str += `${attr.name}:${JSON.stringify(attr.value)},` 
  }
  return `{${str.slice(0,-1)}}`; //去掉最后一个逗号
}

const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;//匹配双括号,内容是表达式的变量
function gen(node){
  if(node.type === 1){//说明是元素
    return codegen(node);
  }else{//文本两种情况
    let text = node.text;
    if(!defaultTagRE.test(text)){//纯文本
      return `_v(${JSON.stringify(text)})`;  
    }else{
      //_v(_s(name)+'hello'+_s(age))
      let tokens = [];
      let match;
      defaultTagRE.lastIndex = 0;//从文本开头执行匹配,每次exec后,lastIndex都会更新为下一次匹配开始的位置
      let lastIndex = 0;//最后匹配的位置
      while(match = defaultTagRE.exec(text)){
        //使用正则来捕获文本
        let index = match.index;//匹配的位置
        if(index > lastIndex){//比如 {{name}} hello {{age}},取得就是 hello 这段
          tokens.push(JSON.stringify(text.slice(lastIndex,index)));
        }
        tokens.push(`_s(${match[1].trim()})`)
        lastIndex = index + match[0].length;//更新最后匹配的位置
      }
      // 防止插入语法后面还存在一些文本
      if(lastIndex < text.length){
        tokens.push(JSON.stringify(text.slice(lastIndex)));
      }
      return `_v(${tokens.join('+')})`
    }
  }
}

function genChildren(children){
  return children.map(item=> gen(item));
}

function codegen(ast){
  let children = genChildren(ast.children);
    let code = (`_c('${ast.tag}',${ast.attrs.length > 0 ? genProps(ast.attrs) : 'null'}${ast.children.length > 0 ? `,${children}` : ',null'})`)
  return code;
}

export function compileToFunction(template){
  //1.将template转化为ast语法树
  let ast = parseHTML(template);
  // console.log(ast);
  // 2.生成render函数(render方法执行后返回的结果是虚拟DOM)
  //将ast树生成为类似于下面的字符串
  //  _c('div',{id:'app'},_c('div',{style:{color:'red'}},_v(_s(name)+'hello'),_c('span',undefine,_v(_s(age)))))
  // 
  let code = codegen(ast); 
  code = `with(this){return ${code}}`
  let render = new Function(code);//根据代码生成render函数
  return render;
}

render函数准备

模版引擎的实现原理就是with + new Function

即在compiler/index.js里面的这个

export function compileToFunction(template){
  //1.将template转化为ast语法树
  let ast = parseHTML(template);
  // console.log(ast);
  // 2.生成render函数(render方法执行后返回的结果是虚拟DOM)
  //将ast树生成为类似于下面的字符串
  //  _c('div',{id:'app'},_c('div',{style:{color:'red'}},_v(_s(name)+'hello'),_c('span',undefine,_v(_s(age)))))
  // 
  let code = codegen(ast); 
  code = `with(this){return ${code}}`
  let render = new Function(code);//根据代码生成render函数
  return render;
}

完成之后需要在init.js里面去执行挂载

import {mountComponent} from './lifeCycle'
//...
Vue.prototype.$mount = function(el){
    //....
    mountComponent(vm,el);
}

lifeCycle.js

export function initLifeCycle(Vue){
  Vue.prototype._updata = function(vnode){
    
  }
  Vue.prototype._render = function(){

  }
}


export function mountComponent(vm,el){
  //调用render方法产生虚拟DOM
  vm._updata(vm._render());//vm.$options.render()渲染虚拟结点,vm._update()生成真实DOM
  // 根据虚拟DOM产生真实DOM
  // 3.插入到el元素中
}

Vue核心流程 1)创造了响应式 2)模版转化为ast语法树 3)将ast语法树转换为render函数 4)后续每次数据更新可以只执行render函数(无需再次执行ast转化过程)

render函数会产生虚拟节点(使用响应式数据)

再根据虚拟节点创造真实的DOM

当渲染函数render执行时,会执行_c,_v,_s等函数

当渲染时,会在事例中取值,我们就可以将属性和视图绑定在一起

_update函数就是将虚拟的DOM结点转化为真实的DOM节点

lifyCycle.js

import { createElementVNode } from "./vdom/index"
import { createTextNodeVNode } from "./vdom/index"

export function initLifeCycle(Vue){
  Vue.prototype._updata = function(vnode){//将虚拟DOM转化为真实DOM
    const vm = this;
    const el = vm.$el;
    //这里vnode是虚拟节点,是真实节点
    patch(el,vnode);//使用vnode,更新出真正的dom
    //patch既有初始化的功能,又有更新的功能
  }
  Vue.prototype._c = function(){
    return createElementVNode(this,...arguments)
  }
  // _c('div',{},...children)
  Vue.prototype._v = function(){
    return createTextNodeVNode(this,...arguments)
  }
  // _v(text)
  Vue.prototype._s = function(value){
    if(typeof value !== 'object')return value;
    return JSON.stringify(value);
  }
  Vue.prototype._render = function(){
    const vm = this;
    // 让with中的this指向vm
    // console.log(vm.name,vm.age);
    return vm.$options.render.call(vm);//通过ast语法转义后生成的render方法
  }
}


export function mountComponent(vm,el){ //这里的el是通过querySelector处理过的
  vm.$el = el;
  //调用render方法产生虚拟DOM
  vm._updata(vm._render());//vm.$options.render()渲染虚拟结点,vm._update()生成真实DOM
  // 根据虚拟DOM产生真实DOM
  // 3.插入到el元素中
}

然后在

vdom/index.js中进行创建虚拟结点

// h函数,_c函数都是调用这些
export function createElementVNode(vm,tag,data,...children){
  if(data==null){
    data = {};
  }
  let key = data.key || null;
  if(key){
  delete data.key;
  }
  return VNode(vm,tag,key,data,children)
}

// _v
export function createTextNodeVNode(vm,text){
  return VNode(vm,undefined,undefined,undefined,undefined,text)
}
// 创建虚拟DOM,和ast一样吗?
// ast是做的语法上的转化,他描述的是语法本身(可以描述js css html)
// 虚拟DOM是描述的dom元素,可以增加一些自定义属性(描述dom)
function VNode(vm,tag,key,data,children,text){
  return {
    vm,tag,key,data,children,text
  }
}

虚拟DOM转化为真实DOM

转化是发生在更新时,初始化也会有更新操作。

只不过初始化的更新,是从旧的DOM转化为由虚拟DOM产生的真实DOM。

后续的更新是diff算法进行比较进而更新

通过xxx.nodeType,如果这个xxx是元素节点上截取下来的那么,其值为1否则就是undefined

新的DOM产生时需要将新的DOM放在旧DOM之后,再将其删除

lifeCycle.js

function createElm(vnode){
  let {tag,data,children,text} = vnode;
  if(typeof tag ==='string'){//标签
    vnode.el = document.createElement(tag);// 这里将真实节点和虚拟节点对应起来,后续如果修改属性了
    patchProps(vnode.el,data);// 处理data
    children.forEach(child=>{
      vnode.el.appendChild(createElm(child));
    });
  }else{//就是创建文本
    vnode.el = document.createTextNode(text);
  }
  return vnode.el;
}

function patchProps(el,props){
  for(let key in props){ //styly{color:'red'}
    if(key === 'style'){
      for(let styleName in props.style){
        el.style[styleName] = props.style[styleName];
      }
    }else{
     el.setAttribute(key,props[key]);//这里将属性都设置到真实dom上
    }
  }
}

function patch(oldVNode,vnode){
  //初渲染和后面的diff渲染一样的
  const isRealElement = oldVNode.nodeType;//nodeType是js原生属性,如果是元素节点的那么值就是1
  if(isRealElement){
    const elm = oldVNode;//这里oldVNode是真实dom
    const parentElm = elm.parentNode;//拿到真实元素

    let newElm = createElm(vnode);
    parentElm.insertBefore(newElm,elm);//将新节点插入到老节点后面
    parentElm.removeChild(elm);//删除老节点
    return newElm;
  }else{
    // diff算法
  } 
}

生成节点操作

依赖收集

dependency collection

我们更新后节点需要自动去完成修改节点的操作,而非人为。即是观察者模式,在该模式下,会监听到数据变化从而更新视图

1.将数据处理为响应式,initState(针对对象来说主要是增加defineProperty,针对数组就是重写方法)

2.模板编译:将模板转为ast语法树,将ast语法树生成render方法

3.调用render函数会进行取值操作产生对应的虚拟DOM render(){_c(‘div’,null,-v(name))}

4.将虚拟DOM转化为真实DOM

一个属性对应一个dep,在一个视图中,一个watcher对应多个dep。(dep是监视属性的东西)

Vue里面是否是在使用数据的地方对应着一个watcher,一个属性对应着处使用,因此一个dep对应多个watcher

每个属性、对象、数组上都有一个 Dep 类型,Dep 类主要就是收集用于渲染的 watcher,Dep相当于发布者,如果有数据变动就调用发布者的更新方法

一个属性可能被多个组件用到,那么一个dep就对应着多个watcher。dep和watcher是多对多的关系

封装一个watcher类,监视数据变化

在挂载组件的的时候就需要给组件一个监视器,那么需要在lifeCycle那里改改

export function mountComponent(vm,el){ //这里的el是通过querySelector处理过的
  vm.$el = el;
  //调用render方法产生虚拟DOM
  const updateComponent = vm._updata(vm._render());//vm.$options.render()渲染虚拟结点,vm._update()生成真实DOM
  // 根据虚拟DOM产生真实DOM
  // 3.插入到el元素中
  new Watcher(vm,updateComponent,true);//这里的true标识着一个渲染过程
}

这里传入true是为了判定是挂载还是更新

通过dep依赖对象和watcher监视属性和组件,那么在修改时就会非常方便

observe/dep.js

let id = 0;
class Dep{
  constructor(){
    this.id = id++;//属性的dep需要收集watcher
    this.subs = [];//存放着当前属性对应的watcher有哪些
  }
  depend(){
    //为了避免一个模板中使用两个数据导致重复收集,除了dep->watcher还希望watcher->dep
    // this.subs.push(Dep.target);//收集watcher这样写会重复

    Dep.target.addDep(this);//收集dep,先让watcher收集到dep,再让dep存储watcher
    // 一个组件中由多个属性组成(那么对应一个watcher监视多个dep)
  }
  addSub(watcher){
    this.subs.push(watcher);
  }
  notify(){
    this.subs.forEach(watcher => watcher.update());//让视图去更新
  }
}

Dep.target = null;

export default Dep;

observe/watcher.js

import Dep from  "./dep";

let id = 0;

// 1)当我们创建渲染watcher的时候我们会把当前的渲染watcher放在Dep.target上
// 2)调用_render()会取值走到get上


class Watcher{//不同组件有不同的watcher
  constructor(vm,fn,options){
    this.id = id++;
    this.renderWatcher = options;
    this.getter = fn;//getter意味调用这个函数可以发生取值操作
    this.deps = [];//收集依赖
    this.depsId = new Set();//收集依赖的id
    this.get();
  }
  get(){
    Dep.target = this;//静态属性只有一份
    this.getter();//会去vm上取值,当渲染时就会有取值操作触发getter,然后在getter里面操作
    Dep.target = null;//渲染完成就清空,只是在模板中收集的时候才会做依赖收集
  }
  addDep(dep){//一个组件对应多个属性,重复的属性无需记录
    let id = dep.id;
    if(!this.depsId.has(id)){
      this.deps.push(dep);
      this.depsId.add(id);
      dep.addSub(this);
    }
  }
  update(){
    this.get();//重新更新
  }
}

//需要给每个属性添加一个dep,目的是收集watcher
// n个dep对应一个视图(一个watcher)
// 一个属性对应多个组件,一个dep对应多个watcher    所以两者关系是多对多

export default Watcher;

dep监视属性,watcher监视一个组件。

在修改属性时,调用dep.notify()方法,这个方法的添加可以在循环给每个值添加监视器的set里面(这样就能在更新后去对应的更新视图)

比如在observe/index.js中的defineProperty函数中

export function defineReactive(data, key, val){//形成闭包,值不会消失
  //如果数据是对象那么再次递归处理进行劫持
  observe(val);
  let dep = new Dep();//每一个属性都有dep
  Object.defineProperty(data,key,{
    get(){//取值会执行get
      if(Dep.target){
        dep.depend();//让这个属性的收集器记住当前的watcher
      }
      return val;
    },
    set(newVal){
      if(newVal !== val){
        val = newVal; 
         dep.notify();//值更新了,通知更新视图
      }
    }
  })
}

当值更新,调用set方法,里面调用dep的notify方法,在此方法中遍历循环subs数组,每个里面的watcher(也就是使用到该属性的组件 )都调用update方法,在update里面调用this.get(),进行更新渲染

每个属性都有一个dep监视(属性就是被观察者),watcher是观察者(属性变化了会通知观察者来更新) ====》观察者模式

但是这个有缺陷,每次更新数据就立即更新页面,应该等待数据更新完毕再更新页面,这样性能更高。

实现异步更新

目的:为了减少渲染次数,期望数据更新完毕,再进行渲染操作

那么可以考虑浏览器对于事件循环的操作。

浏览器都是先处理同步任务,然后执行异步任务,异步任务分为宏任务和微任务,浏览器优先清空微任务队列,再执行定时器等宏任务

同步任务>微任务>宏任务,那么可以利用这个进行更新

渲染放在一个异步任务中,那么只能等所有同步任务执行完毕才会去更新页面。使用watcher里面的更新

observe/watcher.js里面添加一些东西

//.....以前的都一样,下面是改变内容
class Watcher{
  //.....
  update(){
      queueWatcher(this);//将该监视器放入调度队列中
  }
  run(){
      this.get();//真正执行渲染操作
  }
}

let queue = [];//因为可能更新同一属性多次,那么需要去重,只保留最后一个
let has ={};//使用对象去重,或者set去重
let pending = false;//进行防抖操作,无论调用多少次,只执行一次

function flushSchedulerQueue(){
let flushQueue = queue.slice(0);//拷贝一下queue
flushQueue.forEach(q => q.run());//在刷新过程中可能存在新的watcher,重新被放回在队列中
queue = [];
has = {};
pending = false;
}

function queueWatcher(watcher){
let id = watcher.id;//取出每个监视器的唯一标识id
if(has[id] == null){
  queue.push(watcher);
  has[id] = true;
//可能有多个组件不管update多少次,最终只执行一次刷新操作
  if(!pending){
    nextTick(flushSchedulerQueue,0)//刷新调度队列
    pending = true;
  }
}
}

let callbacks = [];
let waiting = false;
function flushCallbacks(){
waiting = false;
let cbs = callbacks.slice(0);
callbacks = [];
cbs.forEach(cb=>cb());
}

let timerFunc; 
if(Promise){
timerFunc = (flushCallbacks)=>{
  Promise.resolve().then(flushCallbacks)
}
}else if(MutationObserver){
let observer = new MutationObserver(flushCallbacks);//这里传入的回调是异步执行的
let textNode = document.createTextNode(1);
observer.observe(textNode,{
  characterData:true
});//让observer监控文本,如果数据变化,那么就执行cb任务
timerFunc = () =>{
  textNode.textContent = 2;
}
}else if(setImmediate){
timerFunc = () =>{
  setImmediate(flushCallbacks);
}
}else{
timerFunc = () =>{
  setTimeout(flushCallbacks,0);
}
}

//nextTick中没有直接使用某个api,而是采用优雅降级的方式
// 内部采用promise(ie不兼容)降级为MutationObserver(h5的api) 可以再降级为ie专享setImmediate   降级为 setTimeout 
export function nextTick(cb){//先内部还是先用户
callbacks.push(cb);//维护nextTick中的callback方法,同步操作
if(!waiting){
  // debugger;
  timerFunc();
  waiting = true;
}
}

这里面就相当于是对应一个watcher 执行一次run

但是这个有个问题,当一个属性被多个组件使用时,更改这个属性,那么还是要执行多次run,而且当使用promise.resolve.then或者同步来获取更新后的数据得到的却得到是更新前的(因为在获取时,页面并没有进行更新)

此时可以定义一个nextTick方法。同时在vue原型上添加。

Vue2在异步更新时是采取的降级方法,Promise => Mutation => setImmediate =>setTimeout

监视数组

上面我们的代码只是重写了数组的方法,没有探究通过数组下标进行改变值是否能监视到

答案是不能

但是如果直接像这样 arr = []是可以更新的,因为劫持到了数组,但是数组内的元素变化没有劫持

给数组和对象本身都增加dep,当他们修改时就可以触发更新了

深层次嵌套会递归处理,递归的话性能就比较差

不存在的属性监控不到,存在的属性要重写方法

observe/index.js

//...在其中的改变一些东西
class Observer{
    constructor(data){
        //添加一个依赖收集器,其他和上面一样
        this.dep = new Dep();
    }
}
//然后再defineProperty里面进行一些改变
export default defineProperty(date,key,val){
    let childOb = observe(val);//observe函数返回的Observer对象
    let dep = new Dep();//每一个属性都有dep
  Object.defineProperty(data,key,{
    get(){//取值会执行get
      if(Dep.target){
        dep.depend();//让这个属性的收集器记住当前的watcher
        if(childOb){//比如对象,数组类
          childOb.dep.depend();//让数组或者对象本身进行依赖收集
          if(Array.isArray(val)){//如果值还是数组
              dependArray(val);
          }
        }
      }
      return val;
    }//set这些和之前一样
  })
}
//多一个函数
function dependArray(val){
    for(let i = 0; i < val.length; i ++){
        let current = val[i];
        current.__ob__.dep.depend();//__ob__是Observer的实例对象,上面有dep收集器,调用其依赖收集的方法
        if(Array.isArray(current)){
            dependArray(current);//如果对象里面套对象再套对象
        }
    }
}

observe/array.js

在这里面添加通知,数组改变了需要重新渲染模版

methods.forEach((method)=>{
    newArrayProto[method] = function (...args){
        
        //上面和之前一样
        ob.dep.notify();//ob是数组的实例对象,然后在上面添加了dep属性
		return result;
    }
})

实现计算属性

如果是

new Vue({
    el:'#app',
    data:{
        firstname:'赤',
        lastname:'橙'
    },
    computed:{
        fullname(){
            return this.firstname + this.lastname;
        }
    }
})

那么就是defineProperty中的get方法

如果是对象写法,那就需要再写

计算属性 依赖的值发生变化才会重新执行用户的方法 那么需要维持一个dirty属性, 默认计算属性不会立刻执行

计算属性就是一个defineProperty

计算属性也是一个watcher,默认渲染会创造 一个渲染watcher,放入队列中,先有渲染watcher,然后是计算属性watcher(使用到数据的地方就是会有watcher,管理每一个watcher的就是dep)

observe/dep.js

//添加一些方法,在暴露之前
let stack = [];
export function pushTarget(watcher){
    let stack = [];
export function pushTarget(watcher){
  stack.push(watcher);
  Dep.target = watcher;
  // 渲染时会将watcher入栈,渲染完就出栈
}

export function popTarget(){
  stack.pop();
  Dep.target = stack[stack.length - 1];
}
export default Dep;

然后在watcher里面也需要修改

class Watcher{
    constructor(vm,fn,options){
        //和之前一样
        this.lazy = options.lazy;
        this.dirty = this.lazy;//缓存值
        this.lazy?undefined:this.get();
        this.vm = vm;//防止在计算属性取得getter时,调用的this不是对应的this
    }
  	//方法只是修改了get和update,其他没有改变,添加了evaluate   
    get(){
        pushTarget();
        let val = this.getter().call(this.vm);//因为调用this.firstname时不使用回调,this会丢失
        popTarget();
        return value;
    }
    evaluate(){
        this.value = this.get();//缓存存储值,多次用到而没有修改的计算属性时,就是拿到的缓存值
        this.dirty = false;
    }
    update(){
        if(this.lazy){//如果是计算属性,依赖属性变化了,就标识是脏值
          this.dirty = true;
        }else{
          queueWatcher(this);//暂存watcher
        // this.get();//重新更新
     }
     depend(){
         let i = this.deps.length;
         while(i--){
             this.deps[i].depend();//让计算属性watcher也收集渲染watcher
         }
     }
  }
}

然后在初始化文件中去修改内容,因为初始化还包括初始化计算属性

计算属性根本不会收集依赖,只会让自己的依赖属性去收集依赖

state.js

export function initState(vm){
  const opts = vm.$options;//获取所有的选项
  if(opts.data){//如果有data选项那么初始化data
    initData(vm);
  }
  if(opts.computed){
    initComputed(vm);
  }
}
//剩下的和之前一样除了我在下面声明的函数


}function initComputed(vm){
    // debugger;
    const computed = vm.$options.computed;
    const watchers = vm._computedWatchers ={};//将计算属性watcher保存到vm上
    //循环对象
    for(let key in computed){
        let userDef = computed[key];
        //userDef 可能是对象可能是函数
        //需要监控计算属性中get的变化,传入值,监视的实例,方法,配置项
        let fn = typeof userDef === 'function' ? userDef :userDef.get;
        watchers[key] = new Watcher(vm,fn,{lazy:true});
        defineComputed(vm,key,userDef);
    }
}
function defineComputed(target,key,userDef){
//   const getter = typeof userDef === 'function' ? userDef :userDef.get;
    const setter = userDef.set || (()=>{});
    Object.defineProperty(target,key,{
        get:createComputedGetter(key),//希望当重复取值时不会调用getter
        set:setter
    })
}

function createComputedGetter(key){
    //我们需要检测是否执行这个getter
    return function(){
        const watcher = this._computedWatchers[key];//获取到对应属性的watcher
        if(watcher.dirty){
            //如果是脏的就去执行用户传入的函数
            watcher.evalaute();//求值后 dirty变为false,下次取值,就不求值了
        }
        if(Dep.target){//计算属性出栈后还有渲染 watcher,应该也让计算属性中的watcher去收集上一层watcher
            watcher.depend();
        }
        return watcher.value;//这样就不用每次取值都是get来取,可以从缓存中来取
    }
}

watch的实现原理

watch写法:1函数,2数组写法

底层最终调用的是$watch写法

在Vue原型链上添加$watch方法

index.js

Vue.prototype.$watch = function (exprOrFn, cb) {//还有deep:true,immediate等
    // firstname
    // ()=>{}
    new Watcher(this, exprOrFn, { user: true },cb);
}

因为这里调用了watcher,而且与之前不同,所以要进行修改

observe/watcher.js

class Watcher{
    constructor(vm,exprOrFn,options,cb){
        this.cb = cb;//对于watch
        if (typeof exprOrFn === 'string') {
            this.getter = function(){return vm[exprOrFn]}//将字符串变为函数
        } else {
            this.getter = exprOrFn;//getter意味调用这个函数可以发生取值操作
        }
        this.user = options.user//标识是否是用户自己的watcher
    }
    run(){
        this.get();
        if(this.user){
            this.cb(  );
        }
    }
}

然后因为watch在一开始就要判断有无,那么在state.js里面要进行修改

export function initState(vm){
    const opts = vm.$options
    if(opts.watch){
        initWatch(vm);
    }
}
function initWatch(vm) {
    let watch = vm.$options.watch;
    for (let key in watch) {//字符串,数组,函数
        const handler = watch[key];

        if (Array.isArray(handler)) {
            for (let i = 0; i < handler.length; i++) {
                createWatcher(vm, key, handler[i]);
            }
        }else{
            createWatcher(vm, key, handler);
        }
    }
}

function createWatcher(vm,key,handler){//字符串,数组,函数(还有可能是对象)
    if(typeof handler === 'string'){
        handler = vm[handler];
    }
    return vm.$watch(key,handler);
}
//这些上面部分是改变内容

实现基本的diff算法

我们之前的更新非常的暴力,直接生成新的虚拟结点,通过新的虚拟姐弟那生成真实节点,生成后替换掉老的节点

但是这样子性能消耗非常大

第一次渲染的时候,我们会产生虚拟节点,第二次更新会调用render方法产生新的虚拟节点。比对出需要更新的内容更新部分内容

先将一些方法从lifeCycle.js放在state.js里面

export function initStateMixin(Vue) {
    Vue.prototype.$nextTick = nextTick;
    Vue.prototype.$watch = function (exprOrFn, cb) {//还有deep:true,immediate等
        // firstname
        // ()=>{}
        new Watcher(this, exprOrFn, { user: true }, cb);
    }
}

将之前渲染节点的函数放在vdom/patch.js里面,顺便更新内容

vdom/patch.js

 export function createElm(vnode){
    //内容省略,除了以下函数改变
     patchProps(vnode.el,{},data)
  }
  
  export function patchProps(el,oldProps = {},props = {}){
      //可能老的属性有而新的属性没有的情况需要去除老的
        let oldStyle = oldProps.style || {};
        let newStyle = props.style || {};
        for (let key in oldStyle) {//老的样式中有而新的样式中没有,则删除
            if (!newStyle[key]) {
                el.style[key] = '';
            }
        }

        for (let key in oldProps) {
            if (!props[key]) {
                el.removeAttribute(key);//老的属性中有而新的没有,则删除属性
            }
        }

    //剩下内容省略(和之前相同)
  }
  
  export function patch(oldVNode,vnode){
    //初渲染和后面的diff渲染一样的
    const isRealElement = oldVNode.nodeType;//nodeType是js原生属性,如果是元素节点的那么值就是1
    if(isRealElement){
      const elm = oldVNode;//这里oldVNode是真实dom
      const parentElm = elm.parentNode;//拿到真实元素
  
      let newElm = createElm(vnode);
      parentElm.insertBefore(newElm,elm);//将新节点插入到老节点后面
      parentElm.removeChild(elm);//删除老节点
      return newElm;
    }else{
      // diff算法
        patchVnode(oldVNode,vnode);
    } 
  }
 function patchVnode(oldVNode, vnode) {
    if (!isSameVnode(oldVNode, vnode)) {
        //使用老节点的父亲进行替换
        let el = createElm(vnode)
        oldVNode.el.parentNode.replaceChild(el, oldVNode.el)
        return el;
    }
    //文本情况,期望对文本内容进行比较
    let el = vnode.el = oldVNode.el //复用老节点的元素
    if (!oldVNode.tag) {//是文本
        if (oldVNode.text !== vnode.text) {
            el.textContent = vnode.text;//用新的文本覆盖掉老的
        }
    }
    //是标签        需要比对标签的属性
    patchProps(el, oldVNode.data, vnode.data);

    //比较儿子节点      比较一方有儿子,一方没有儿子        两方都有儿子

    let oldChildren = oldVNode.children || [];//防止取到的是一个值
    let newChildren = vnode.children || [];

    if (oldChildren.length > 0 && newChildren.length > 0) {
        //完整的diff算法(需要比较两个人的儿子)
        updateChildren(el, oldChildren, newChildren);
    } else if (newChildren.length > 0) {//没有老的儿子直接插入
        mountChildren(el, newChildren);
    } else if (oldChildren.length > 0) {//新的没有,老的有,需要删除
        el.innerHTML = '';//也可以循环删除
    }

    return el;
}

function mountChildren(el, newChildren) {
    for (let i = 0; i < newChildren.length; i++) {
        let child = newChildren[i];
        el.appendChild(createElm(child));
    }
}
function updateChildren(el, oldChildren, newChildren) {
    //比较两个儿子的时候,为了增高性能会有优化手段
    let oldStartIndex = 0;//老儿子开始的位置
    let newStartIndex = 0;//新儿子开始的位置
    let oldEndIndex = oldChildren.length - 1;//老儿子结束的位置
    let newEndIndex = newChildren.length - 1;//新儿子结束的位置
    let oldStartVnode = oldChildren[oldStartIndex];
    let oldEndVnode = oldChildren[oldEndIndex];
    let newStartVnode = newChildren[newStartIndex];
    let newEndVnode = newChildren[newEndIndex];


    function makeIndexByKey() {
        let map = {};
        children.forEach((child, index) => {
            map[child.key] = index;
        })
        return map;
    }
    let map = makeIndexByKey(oldChildren);


    while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
        if (!oldStartVnode) {
            oldStartVnode = oldChildren[++oldStartIndex];
        } else if (!oldEndVnode) {
            oldEndVnode = oldChildren[--oldEndIndex];
        }
        //有一方大于尾指针就停止
        else if (isSameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode)// 如果是相同节点,则递归比较子节点
            oldStartVnode = oldChildren[++oldStartIndex];
            newStartVnode = newChildren[++newStartIndex];
        }
        else if (isSameVnode(oldEndVnode, newEndVnode)) {//比较尾节点
            patchVnode(oldStartVnode, newStartVnode)// 如果是相同节点,则递归比较子节点
            oldEndVnode = oldChildren[--oldEndIndex];
            newEndVnode = newChildren[--newEndIndex];
        }

        // 交叉比较 abcd -> dabc
        // 头尾比对和尾头比对,同时处理的倒序的情况
        else if (isSameVnode(oldEndVnode, newStartVnode)) {
            patchVnode(oldEndVnode, newStartVnode);
            el.insertBefore(oldEndVnode.el, oldStartVnode.el)//将老的后面的节点插入到开头节点的前面

            oldEndVnode = oldChildren[--oldEndIndex];
            newStartVnode = newChildren[++newStartIndex];

        }
        else if (isSameVnode(oldStartVnode, newEndVnode)) {
            patchVnode(oldStartVnode, newEndVnode);
            el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling)//将老头节点放在尾节点之后
            oldStartVnode = oldChildren[++oldStartIndex];
            newEndVnode = newChildren[--newEndIndex];
        }
        // 在给动态列表添加key时,尽可能避免使用索引,无论你怎么改变索引都是从0开始非常容易错乱
        else {
            // 乱序比对
            let moveIndex = map[newStartVnode.key];//如果拿到则说明是要移动的索引
            if (moveIndex !== undefined) {
                let moveVnode = oldChildren[moveIndex];//找到对应的虚拟节点 ,复用
                el.insertBefore(moveVnode.el, oldStartVnode.el);
                oldChildren[moveIndex] = undefined//标识这个节点清空了
                patch(moveVnode, newStartVnode);
            } else {//找不到的情况
                el.insertBefore(createElm(newStartVnode), oldStartVnode.el);
            }
        }

    }
    if (newStartIndex <= newEndIndex) {//多余的塞进去
        for (let i = newStartIndex; i <= newEndIndex; i++) {
            let childEl = createElm(newChildren[i]);
            // 可能像后追加,可能向前追加
            // el.appendChild(childEl)
            let anchor = newChildren[newEndIndex + 1] ? newChildren[newEndIndex + 1].el : null; //获取下一个元素
            el.insertBefore(childEl, anchor);//当anchor为null的时候,就会认为是appendChild
        }
    }
    if (oldStartIndex <= oldEndIndex) {
        for (let i = oldStartIndex; i <= oldEndIndex; i++) {
            if(oldChildren[i]){
                el.removeChild(oldChildren[i].el);
            }
        }
    }
}

每次获取dom需要计算位置,因此比较消耗性能。希望比较两个节点的差异之后再进行替换

diff算法是一个评级比较的过程,父亲和父亲比,儿子和儿子比

比较节点:不是同一节点直接替换(删除老的,换上新的)。

两个节点是同一个节点(判断节点的tag和节点的key),比较两个节点的属性是否由差异(复用老节点,将差异的属性更新)

文本进行特判,先判断是否有tag属性,文本是没有的,然后再进行比对,如果不同,那么使用新节点的内容替换掉老节点的内容

Vue2中通过双指针的方式比较两个节点(子节点 )

实现组件的虚拟节点

lifeCycle.js里面将第一次产生的虚拟结点保存到_vnode上。更新 _update函数

export function initLifeCycle(Vue){
    Vue.prototype._update = function(vnode){
        const vm = this;
        const el = vm.$el;
        //这里vnode是虚拟节点,是真实节点
        const preVnode = vm._vnode;
        vm._vnode = vnode;//把组件第一次产生的虚拟节点保存到_vnode上
        if(preVnode){//之前渲染 
            vm.$el = patch(preVnode,vnode)
        }else{
            vm.$el = patch(el,vnode);//使用vnode,更新出真正的dom
        }
        //patch既有初始化的功能,又有更新的功能
    }
}

组件的定义方式:全局注册和局部注册

Vue.component('my-button',{
	template:`<button></button>`
})

new Vue({
    el:'#app',
    component:{
        'mu-button':{
            template:`<button></button>`
        }
    }
})

其查找方式类似于JS的原型链,先在自身找,找不到再往外找。

实际上是调用的Vue.extend这个API

Vue.component('my-button',Vue.extend({template:'<button></button>'}))

创建组件就相当于创建一个子类

在globalAPI里面(改自Vue源码)

import { observe } from './observe/index'
import {mergeOptions} from './utils'

export function initGlobalAPI(Vue) {
  // config
  const configDef = {}
  configDef.get = () => config
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.


  // 2.6 explicit observable API
  Vue.observable = function(obj){
    observe(obj)
    return obj
  }

  Vue.options = Object.create(null)
  

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options = {
    _base:Vue,
  }
  Vue.mixin = function(mixin) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
  Vue.extend = function(options) {
    //实现根据用户的参数返回一个构造函数
    function Sub(options = {}){
      //调用Vue的构造函数
      this._init(options)
    }
    Sub.prototype = Object.create(this.prototype)//Sub.protoytype.__proto__ = Vue.protoType
    Sub.prototype.constructor = Sub
    //将用户传递的参数和全局的Vue.options来合并
    Sub.options = mergeOptions(Vue.options, options);
    return Sub
  }
  Vue.options.components = {}//全局的指令  Vue.options.directives
  Vue.components = function(id, definition){
    //如果是函数直接返回,不是函数就进行包装
    definition = typeof definition === 'function' ? definition : Vue.extend(definition)
    Vue.options.components[id] = definition
  }

}

utils.js

export function mergeOptions(ops = {}, mixin) {
    // 创建一个空对象作为合并后的结果
    const options = {};
    // 定义一个合并策略对象,存放不同属性名对应的合并策略函数
    const strats = {};
    // 定义一个默认的合并策略函数,如果没有找到对应的策略函数,就使用它
    const defaultStrat = function (parentVal, childVal) {
      // 如果子选项有值,就使用子选项的值,否则就使用父选项的值
      return childVal === undefined ? parentVal : childVal;
    };
    // 定义一个合并字段的函数,用于遍历属性并调用合并策略函数
    function mergeField(key) {
      // 根据属性名查找合并策略对象,如果没有找到,就使用默认的合并策略函数
      const strat = strats.hasOwnProperty(key) ? strats[key] : defaultStrat;
      // 调用合并策略函数,传入父选项和子选项的属性值,以及当前的属性名
      options[key] = strat(ops[key], mixin[key], key);
    }
    // 遍历父选项的所有属性,并调用合并字段的函数
    for (let key in ops) {
      mergeField(key);
    }
    // 遍历子选项的所有属性,并调用合并字段的函数
    for (let key in mixin) {
      // 如果父选项没有该属性,才需要调用合并字段的函数
      if (!Object.hasOwn(ops, key)) {
        mergeField(key);
      }
    }
    // 返回合并后的结果对象
    return options;
  }

在init.js里面也需要做出小小的改变,因为有多个组件(多个vue)那么就需要合并选项

export function initMixin(Vue){
    Vue.prototype._init = function(options){
        const vm = this;
        vm.$options = mergeOptions(this.constructor.options,options)
        
        //其他的不变
    }
}

创建完之后要对组件和标签进行一个区分,那么在以前生成虚拟结点的地方就存在一些问题。

那么需要判定是否为真实的标签

那么在vdom/index.js里面就要进行改变

const isReservedTag = (tag) => {
    return [
        "a",
        "ul",
        "ol",
        "li",
        "div",
        "span",
        "p",
        "img",
        "input",
        "button",
        "textarea",
        "h1",
        "h2",
        "h3",
        "h4",
        "h5",
        "h6",
        "table",
        "tr",
        "td",
        "th",
        "tbody",
        "thead",
        "tfoot",
        "tr",
        "th",
        "td",
        "select",
        "option",
        "form",
    ].includes(tag);
};
export function createElementVNode(vm, tag, data, ...children) {
    if (data == null) {
        data = {};
    }
    let key = data.key || null;
    if (key) {
        delete data.key;
    }

    //判断是否为原生标签
    if(isReservedTag(tag)){
        return VNode(vm, tag, key, data, children);
    }else{
        //创建组件的虚拟结点
        // 需要包含组件的构造函数
        let Ctor = vm.$options.components[tag];
        // Ctor可能是组件的定义,可能是一个Sub类,还有可能是组件的component选项
        return createComponentVnode(vm,tag,key,data,children,Ctor);
    }
}

function createComponentVnode(vm,tag,key,data,children,Ctor){
    if(typeof Ctor === 'Object'){
        Ctor = vm.$options._base.extend(Ctor);//将对象转化一下得到构造函数,_base声明于globalAPI上面的
    }
    data.hook = {
        init(){//稍后创造真实节点的时候,如果是组件则调用此init方法

        }
    }
    return VNode(vm,tag,key,data,children,null,{Ctor})
}
//然后VNode代码需要重构一下,因为之前没有处理component,多了一个componentOptions的选项
function VNode(vm, tag, key, data, children, text, componentOptions) {
    return {
        vm,
        tag,
        key,
        data,
        children,
        text,
        componentOptions//组件的构造函数
    };
}

实现组件的渲染流程

vdom/index.js里面继续完善

function createComponentVnode(vm, tag, key, data, children, Ctor) {
    if (typeof Ctor === 'object') {
        Ctor = vm.$options._base.extend(Ctor);//将对象转化一下得到构造函数,_base声明于globalAPI上面的
    }
    data.hook = {
        init(vnode) {//稍后创造真实节点的时候,如果是组件则调用此init方法
            //保存组件的实例到虚拟节点
            let instance = vnode.componentInstance = new vnode.componentOptions.Ctor
            instance.$mount()
        }
    }
    return VNode(vm, tag, key, data, children, null, { Ctor })
}

vdom/patch.js里面,需要修改createElm函数,因为还需要判定组件和元素

function createComponent(vnode) {
    let i = vnode.data
    if((i = i.hook) && (i = i.init)){
         i(vnode)
    }
    if(vnode.componentInstance){
        return true // 说明是组件
    }
}

export function createElm(vnode) {
    let { tag, data, children, text } = vnode;
    if (typeof tag === 'string') {//标签
        //创建真实元素也要区分组件还是元素
        if(createComponent(vnode)){
            //组件
			return vnode.componentInstance.$el//返回组件对应的真实元素
        }
        vnode.el = document.createElement(tag);// 这里将真实节点和虚拟节点对应起来,后续如果修改属性了
        patchProps(vnode.el, {}, data);// 处理data
        children.forEach(child => {
            vnode.el.appendChild(createElm(child));
        });
    } else {//就是创建文本
        vnode.el = document.createTextNode(text);
    }
    return vnode.el;
}

然后再判定createComponent时,就会进入data.hook对象里面的init()组件渲染函数

因为Ctor是构造函数,那么就调用new方法,将实例保存下来,然后进行挂载。挂载时,因为传入的为空,那么就会进入到mountComponent(vm,el)方法,然后el仍然没有值,调用vm._update,进入其中后,调用patch方法,因为patch(el,vnode),el为空,那么就需要在vdom/patch.js上面修改以下patch方法

export function patch(oldVNode, vnode) {
     if(!oldVNode){ //这就是组件的挂载
        return createElm(vnode)//vm.$el  对应的就是组件渲染的结果了
    }
    //后面都一样
}

因为patch的返回值就是挂载的vm.$el,那么在实例instance上就多了一个 $el

组件的真实元素是template生成render之后 , Vnode生成的真实节点

源码剖解

找到Vue打包的入口

  • 使用npm install安装依赖
  • 代码的目录结构:
  • bechmarks性能测试的
  • dist最终打包的结果
  • examples官方的例子
  • flow类型检测(现在被ts代替了)
  • packages一些写好的包
  • scripts所有打包的脚本
  • src源代码目录 compiler专门做模板编译的
  • core Vue2的核心代码
  • platform
  • server服务端渲染相关的
  • sfc解析单文件组件的

通过package.json 找到打包入口

scripts/config.js (full-dev runtime-cjs-dev runtime-esm.......)

web-runtime(运行时 无法解析new Vue传入的template) web-full(runtime + 模板解析) compiler(只有compiler)

cjs esm(支持import 和export 导入模块) browser umd

cjs使用的 require导入模块,导入的模块时拷贝形式,如果修改其中的值,导入的东西并不会改变

esm导入的是地址,那么进行操作修改包内的数据时,再次使用得到的是修改后的数据

cjs

// cjs_module1.js
var count = 1;
function incCount() {
    count += 1;
}

module.exports = {
    count: count,
    incCount: incCount,
}

// cjs_demo.js
var { count, incCount } = require('./cjs_module1.js');

console.log(count); // 1
incCount();
console.log(count); // 1

esm

// esm_module1.js
let count = 1;
function incCount() {
    count += 1;
}

export {
    count,
    incCount,
}

// esm_demo.js
import { count, incCount } from './esm_module1.js';

console.log(count); // 1
incCount();
console.log(count); // 2

在html中使用esm,type=“module”是关键

<script src="./esm_main.js" type="module"></script>
  • 打包的入口
    • src/platforms/web/entry-runtime.js

      src/platforms/web/entry-runtime-with-compiler.js(两个入口的区别是带有compiler的会重写$mount,将template变成render函数)

      runtime/index.js(所谓的运行时,会提供一些操作DOM的API 、属性操作、元素操作,提供一些组件和指令 )

分析全局API

对于里面的方法如何执行

(1)了解核心流程,单独打开源码去看

(2)不知道流程,可以通过测试样例,或者自己写一些样例进行实现

指定sourcemap参数 可以开启代码调试

比如在package.json的script里面,scripts/config.js后面加入 -s 或者 -sourcemap

vue.config

在globalAPI里面 Object.defineProperty(Vue.'config',configDef)进行配置信息

Vue.util装载的工具方法,比如warn,extend,mergeOptions,defineReactive(extend合并操作,mergeOptions合并策略,defineReactive定义响应式)

set,delete,nextTick 2.6新增了observable让一个对象变成响应式

ASSET_TYPES里面存入了,components,directives,filter这些

还有mixin,use,extend

响应式数组的实现原理

Vue2响应式数据的理解

可以监控一个数据的修改和获取操作,针对对象格式给每个对象的属性进行劫持 使用 Object.defineProperty

源码层面:initData -> observe ->defineReactive方法(内部对所有属性进行重写,因此存在性能问题),递归的给每个属性添加getter和setter

我们使用Vue的时候如果层级过深(考虑性能),如果数据不是响应式就不要放在data中了。属性取值,尽可能避免多次取值。如果有些对象放在data中但是不是响应式的,可以使用Object.freeze()进行冻结

Vue2如何检测数组的变化

Vue2中没有使用defineProperty进行检测,因为直接修改索引的情况很少,通常是使用push,pop等方法进行修改,那么就需要对这些方法进行重写

并且对每个数据进行检测。

注意这里重写是针对的每一个在data里面的数组,并没有在Array的原型链上直接覆盖。

还有通过索引进行修改数据无法进行实时渲染,比如arr[1]=100;arr.length = 300

但是arr[0].x=100是可以的,因为可以监视对象的每一个属性的修改和取值操作。因此会触发更新

Vue如何进行依赖收集——观察者模式

  • 被观察者指代的是数据(dep),观察者(Watcher,3种,渲染watcher,计算属性 ,用户watcher)

  • 一个watcher中可能有多个数据,因此watcher中还需要保存dep。

  • 多对多的关系,一个dep对应多个watcher,一个watcher会有多个dep。默认是在渲染时,进行依赖收集

  • image-20231116143153843

计算属性和清理会用到watcher上的dep。

在进行取值的时候,watcher收集dep(相当于粉丝订阅自己喜欢的人一样)

image-20231116161449833

image-20231116161711145

image-20231116161753950

然后让dep收集watcher(相当于up主得到粉丝名单),以便up主更新时去通知粉丝去看最新视频(也就是属性改变,通知相应的watcher进行一个更新)

取值的时候收集依赖,设置值的时候更新image-20231116162524940

为什么要清理watcher,比如使用v-if的指令第一次渲染了name第二次渲染的age那么进行清理,当name更新时,会通知视图进行更新,但是此时没用到这个值,也没必要更新

编译原理:用户传递的template属性,需要将这个template编译为render函数。

  • template -> ast语法树
  • 对语法树进行标记(标记的是静态节点)
  • 将ast语法树生成render函数

最终每次渲染可以调用render函数返回对应的虚拟结点(递归是先子后父)

生命周期的实现原理

内部利用发布订阅模式,将用户写的钩子维护称了一个数组,后续一次调用 callHook

策略模式

渲染顺序:父->子->子完->父完那么子组件挂载了之后父组件才会挂载完毕

生命周期钩子:beforeCreate created beforeMount mounted beforeUpdate updated actived deactived beforeDestory destoryed errorCaptured

在beforeCreate中实现的initEvent(初始化$ on ,off,emit等事件 )和initLifecycle(因为什么事情都没有干,因此Vue3中直接取代了)

在create中实现了 initInjections(inject)和initState(响应式数据处理),InitProvide(provide),可以拿到响应式数据而且不涉及到dom渲染,这个api可以在服务端渲染中使用

beforreUpdate每次更新之前调用,在清空队列的时候,会去调用各个watcher的beforUpdate方法

beforDestory手动移除能触发:销毁的时候,是销毁watcher而不是销毁DOM(react是销毁DOM)

destroyed:触发在:路由切换,v-if切换组件,:is动态绑定组件